From 321f9d1dbe5b0528ff8f29d0b6114e4a601fa14e Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Mon, 30 Mar 2026 10:35:25 -0400 Subject: [PATCH 01/21] Initial C# SDK --- .github/workflows/deploy.yml | 46 + .github/workflows/test.yml | 31 + .gitignore | 3 +- CONTRIBUTING.md | 8 +- README.md | 63 +- dotnet/.config/dotnet-tools.json | 13 + dotnet/.csharpierignore | 3 + dotnet/.gitignore | 3 + dotnet/OrchestrateSDK.DotNet.sln | 27 + .../CareEvolution.Orchestrate/BatchRequest.cs | 6 + .../BatchResponse.cs | 6 + .../CareEvolution.Orchestrate.csproj | 26 + .../ClassifyConditionRequest.cs | 10 + .../ClassifyConditionResponse.cs | 27 + .../ClassifyMedicationRequest.cs | 10 + .../ClassifyMedicationResponse.cs | 14 + .../ClassifyObservationRequest.cs | 10 + .../ClassifyObservationResponse.cs | 18 + .../CareEvolution.Orchestrate/ConvertApi.cs | 349 ++ .../ConvertCdaToFhirR4Request.cs | 16 + .../ConvertCdaToHtmlRequest.cs | 6 + .../ConvertCdaToPdfRequest.cs | 6 + .../ConvertCombineFhirR4BundlesRequest.cs | 12 + .../ConvertFhirDstu2ToFhirR4Request.cs | 6 + .../ConvertFhirR4ToCdaRequest.cs | 6 + .../ConvertFhirR4ToHealthLakeRequest.cs | 6 + .../ConvertFhirR4ToManifestRequest.cs | 14 + .../ConvertFhirR4ToNemsisV34Request.cs | 6 + .../ConvertFhirR4ToNemsisV35Request.cs | 6 + .../ConvertFhirR4ToOmopRequest.cs | 6 + .../ConvertFhirStu3ToFhirR4Request.cs | 6 + .../ConvertHl7ToFhirR4Request.cs | 16 + .../ConvertRequestFactory.cs | 16 + .../ConvertX12ToFhirR4Request.cs | 12 + .../EnvironmentConfiguration.cs | 121 + .../Exceptions/OrchestrateClientError.cs | 18 + .../Exceptions/OrchestrateError.cs | 3 + .../Exceptions/OrchestrateHttpError.cs | 9 + .../GetFhirR4CodeSystemRequest.cs | 12 + .../GetFhirR4ValueSetRequest.cs | 6 + .../GetFhirR4ValueSetsByScopeRequest.cs | 12 + .../CareEvolution.Orchestrate/GlobalUsings.cs | 8 + .../IOrchestrateHttpClient.cs | 25 + .../Identity/AddMatchGuidanceRequest.cs | 6 + .../Identity/AddMatchGuidanceResponse.cs | 6 + .../AddOrUpdateBlindedRecordRequest.cs | 10 + .../Identity/AddOrUpdateRecordRequest.cs | 10 + .../Identity/AddOrUpdateRecordResponse.cs | 6 + .../Identity/Advisories.cs | 6 + .../Identity/BlindedDemographic.cs | 8 + .../Identity/DatasourceOverlapRecord.cs | 10 + .../Identity/DeleteRecordResponse.cs | 6 + .../Identity/Demographic.cs | 43 + .../Identity/GetPersonByIdRequest.cs | 6 + .../Identity/HashDemographicResponse.cs | 6 + .../Identity/IdentifierMetricsRecord.cs | 12 + .../Identity/IdentifierMetricsResponse.cs | 16 + .../Identity/IdentityApi.cs | 113 + .../Identity/IdentityMonitoringApi.cs | 27 + .../Identity/LocalHashingApi.cs | 28 + .../MatchBlindedDemographicsResponse.cs | 9 + .../Identity/MatchDemographicsResponse.cs | 11 + .../Identity/MatchGuidanceRequest.cs | 10 + .../Identity/MatchedPersonReference.cs | 8 + .../Identity/OverlapMetricsResponse.cs | 6 + .../Identity/Person.cs | 12 + .../Identity/PersonStatus.cs | 8 + .../Identity/Record.cs | 8 + .../Identity/RemoveMatchGuidanceResponse.cs | 6 + .../Identity/SourceTotal.cs | 8 + .../CareEvolution.Orchestrate/InsightApi.cs | 35 + .../InsightRiskProfileRequest.cs | 12 + .../src/CareEvolution.Orchestrate/Options.cs | 28 + .../OrchestrateApi.cs | 38 + .../OrchestrateHttpClient.cs | 452 +++ .../CareEvolution.Orchestrate/Registration.cs | 33 + .../ResolvedConfiguration.cs | 9 + .../CareEvolution.Orchestrate/ResponseKind.cs | 8 + .../CareEvolution.Orchestrate/RouteBuilder.cs | 29 + .../StandardizeRequest.cs | 12 + .../StandardizeResponse.cs | 6 + .../StandardizeResponseCoding.cs | 10 + .../SummarizeFhirR4CodeSystemRequest.cs | 6 + .../SummarizeFhirR4ValueSetRequest.cs | 6 + .../SummarizeFhirR4ValueSetScopeRequest.cs | 6 + .../TerminologyApi.cs | 451 +++ .../TranslateFhirR4ConceptMapRequest.cs | 8 + .../ApiSurfaceTests.cs | 344 ++ .../AssemblyInfo.cs | 3 + .../CareEvolution.Orchestrate.Tests.csproj | 32 + .../ConfigurationTests.cs | 134 + .../GlobalUsings.cs | 5 + .../Helpers/EnvironmentVariableScope.cs | 23 + .../Helpers/FakeHttpMessageHandler.cs | 107 + .../Helpers/LiveClients.cs | 10 + .../Helpers/LiveTestAttributes.cs | 170 + .../Helpers/LiveTestData.cs | 84 + .../IdentityApiTests.cs | 158 + .../LiveApiTests.cs | 1084 ++++++ .../LiveData/cda.xml | 70 + .../LiveData/dstu2_bundle.json | 2070 +++++++++++ .../LiveData/encoding_cda.xml | 46 + .../LiveData/hl7.txt | 22 + .../LiveData/nemsis_bundle.json | 3236 +++++++++++++++++ .../LiveData/r4_bundle.json | 65 + .../LiveData/risk_profile_bundle.json | 350 ++ .../LiveData/stu3_bundle.json | 2611 +++++++++++++ .../LiveData/x12.txt | 44 + .../LiveIdentityApiTests.cs | 267 ++ .../LiveLocalHashingApiTests.cs | 41 + 110 files changed, 13545 insertions(+), 12 deletions(-) create mode 100644 dotnet/.config/dotnet-tools.json create mode 100644 dotnet/.csharpierignore create mode 100644 dotnet/.gitignore create mode 100644 dotnet/OrchestrateSDK.DotNet.sln create mode 100644 dotnet/src/CareEvolution.Orchestrate/BatchRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/BatchResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyConditionRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyConditionResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyObservationRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ClassifyObservationResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertCdaToFhirR4Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertCdaToHtmlRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertCdaToPdfRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertCombineFhirR4BundlesRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirDstu2ToFhirR4Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToCdaRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToHealthLakeRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToManifestRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV34Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV35Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToOmopRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertFhirStu3ToFhirR4Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertHl7ToFhirR4Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertRequestFactory.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ConvertX12ToFhirR4Request.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/GetFhirR4CodeSystemRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetsByScopeRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/GlobalUsings.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateBlindedRecordRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/Advisories.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/BlindedDemographic.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/DatasourceOverlapRecord.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/DeleteRecordResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/Demographic.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/GetPersonByIdRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/HashDemographicResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsRecord.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/IdentityMonitoringApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/MatchBlindedDemographicsResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/MatchDemographicsResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/MatchGuidanceRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/MatchedPersonReference.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/OverlapMetricsResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/Person.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/PersonStatus.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/Record.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/RemoveMatchGuidanceResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Identity/SourceTotal.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/InsightApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/InsightRiskProfileRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Options.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Registration.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ResolvedConfiguration.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/ResponseKind.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/RouteBuilder.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/StandardizeRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/StandardizeResponse.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/StandardizeResponseCoding.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4CodeSystemRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetScopeRequest.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/TranslateFhirR4ConceptMapRequest.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/AssemblyInfo.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/CareEvolution.Orchestrate.Tests.csproj create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/GlobalUsings.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/EnvironmentVariableScope.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/FakeHttpMessageHandler.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestAttributes.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestData.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/cda.xml create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/dstu2_bundle.json create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/encoding_cda.xml create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/hl7.txt create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/nemsis_bundle.json create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/r4_bundle.json create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/risk_profile_bundle.json create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/stu3_bundle.json create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/x12.txt create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 29caf63..ec8fb04 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -164,3 +164,49 @@ jobs: poetry publish --build --no-interaction -r pypi $version = poetry version Write-Host "Published ``$version`` to Pypi." >> $Env:GITHUB_STEP_SUMMARY + + csharp: + name: CSharp + permissions: + id-token: write + contents: read + packages: write + needs: + - test + - version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup .NET + uses: actions/setup-dotnet@6d38f516158ab5bb06840831c7a2ec4f2dd7ddc1 # v5.0.0 + with: + dotnet-version: | + 8.0.x + 10.0.x + - name: Restore dotnet tools + working-directory: dotnet + run: dotnet tool restore + - name: Check formatting + working-directory: dotnet + run: dotnet csharpier check . + - name: Restore dependencies + working-directory: dotnet + run: dotnet restore OrchestrateSDK.DotNet.sln + - name: Pack + working-directory: dotnet + run: dotnet pack src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj --configuration Release --no-restore -p:Version=${{ needs.version.outputs.version }} -o ./artifacts + - name: Upload package artifact + if: needs.version.outputs.target == 'dev' + uses: actions/upload-artifact@v4 + with: + name: csharp-nuget-package-${{ needs.version.outputs.version }} + path: dotnet/artifacts/*.nupkg + - name: Publish NuGet + if: needs.version.outputs.target == 'prod' + working-directory: dotnet + run: | + dotnet nuget push ./artifacts/*.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --skip-duplicate + echo "Published `${{ needs.version.outputs.version }}` to NuGet.org." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92719c8..82135eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,3 +117,34 @@ jobs: - name: Test working-directory: python run: poetry run pytest -m "${{ (inputs.suite || 'default') }}" + + csharp: + name: CSharp + runs-on: ubuntu-latest + needs: + - python + strategy: + fail-fast: true + matrix: + dotnet-version: ["8.0.x", "10.0.x"] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup .NET + uses: actions/setup-dotnet@6d38f516158ab5bb06840831c7a2ec4f2dd7ddc1 # v5.0.0 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Restore dotnet tools + working-directory: dotnet + run: dotnet tool restore + - name: Check formatting + working-directory: dotnet + run: dotnet csharpier check . + - name: Restore dependencies + working-directory: dotnet + run: dotnet restore OrchestrateSDK.DotNet.sln + - name: Build + working-directory: dotnet + run: dotnet build OrchestrateSDK.DotNet.sln --configuration Release --no-restore + - name: Test + working-directory: dotnet + run: dotnet test OrchestrateSDK.DotNet.sln --configuration Release --no-build diff --git a/.gitignore b/.gitignore index 00a600b..9e44016 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ **/.mypy_cache/ **/.pytest_cache/ **/.ipynb_checkpoints/ -**/__pycache__/ \ No newline at end of file +**/__pycache__/ +.codex diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7e8822..1704926 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,15 @@ # Orchestrate SDK Contribution Guide -The Orchestrate SDK is a TypeScript and JavaScript library for interacting with the Orchestrate API at . Releases are tagged and generated from `main`. Development should be forked from `main` and a PR created to merge back into it. +The Orchestrate SDK is a TypeScript, Python, and C# library for interacting with the Orchestrate API at . Releases are tagged and generated from `main`. Development should be forked from `main` and a PR created to merge back into it. ## Installation -For TypeScript, navigate to the `./typescript` directory and install dependencies with `npm i`. For Python, navigate to the `./python` directory and install dependencies with `poetry install`. +For TypeScript, navigate to the `./typescript` directory and install dependencies with `npm i`. +For Python, navigate to the `./python` directory and install dependencies with `poetry install`. +For C#, navigate to the `./dotnet` directory and restore dependencies with `dotnet restore`. ## Tests -TypeScript tests can be run and watched with `npm run test:watch`. Python tests can be run with `poetry run pytest`. +TypeScript tests can be run and watched with `npm run test:watch`. Python tests can be run with `poetry run pytest`. C# tests can be run with `dotnet test`. To run Local Hashing Service tests, docker must be installed and running. Tests against the Identity API do not use data directly from the Local Hashing Service, so any valid hash key can be used to start the container. See the [Orchestrate Docs](https://orchestrate.docs.careevolution.com/identity/local_hash/hosting.html) for information on starting the container. diff --git a/README.md b/README.md index dd711ca..376c652 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Orchestrate SDK -The Orchestrate SDK is a TypeScript and JavaScript library for interacting with the Orchestrate API at . +The Orchestrate SDK provides TypeScript, Python, and C# clients for interacting with the Orchestrate API at . Full documentation of the API is available at . @@ -18,9 +18,15 @@ Python: pip install orchestrate-api ``` +C#: + +```bash +dotnet add package CareEvolution.Orchestrate +``` + ## Usage -TypeScript: +### TypeScript ```typescript import { OrchestrateApi } from '@careevolution/orchestrate'; @@ -32,7 +38,7 @@ await orchestrate.terminology.classifyCondition({ }); ``` -Python: +### Python ```python from orchestrate import OrchestrateApi @@ -41,11 +47,37 @@ api = OrchestrateApi(api_key="your-api-key") api.terminology.classify_condition(code="119981000146107", system="SNOMED") ``` +### C\# + +```csharp +using CareEvolution.Orchestrate; + +var api = new OrchestrateApi(new OrchestrateClientOptions +{ + ApiKey = "your-api-key", +}); + +await api.Terminology.ClassifyConditionAsync(new ClassifyConditionRequest +{ + Code = "119981000146107", + System = "SNOMED", +}); +``` + +Additionally, C# also supports dependency injection with `IOrchestrateApi` and `OrchestrateApi` registered in the service collection. + +```csharp +using CareEvolution.Orchestrate; + +var services = new ServiceCollection(); +services.AddOrchestrateApi(); +``` + ## Configuration The SDK supports environment variables for configuring HTTP behavior. These can be used for local development, CI, or shared runtime configuration. -For the primary `OrchestrateApi` clients in both TypeScript and Python: +For the primary `OrchestrateApi` clients in TypeScript, Python, and C#: | Environment variable | Purpose | Default | | --- | --- | --- | @@ -60,7 +92,7 @@ Environment variables used by the identity clients: | --- | --- | | `ORCHESTRATE_IDENTITY_URL` | Base URL for `IdentityApi`. Required unless the URL is passed directly when creating the client. | | `ORCHESTRATE_IDENTITY_API_KEY` | API key sent as the `x-api-key` header for `IdentityApi`. | -| `ORCHESTRATE_IDENTITY_METRICS_KEY` | Metrics key sent as the `Authorization` header for `IdentityApi`. A value with or without the `Basic ` prefix is accepted. | +| `ORCHESTRATE_IDENTITY_METRICS_KEY` | Metrics key sent as the `Authorization` header for `IdentityApi`. A value with or without the `Basic` prefix is accepted. | | `ORCHESTRATE_IDENTITY_LOCAL_HASHING_URL` | Base URL for `LocalHashingApi`. Required unless the URL is passed directly when creating the client. | ### Configuration Precedence @@ -71,13 +103,13 @@ When the same setting is provided in more than one place, the SDK resolves it in 2. The matching environment variable 3. The SDK default, when one exists -For example, passing `api_key` or `timeout_ms` in Python, or `apiKey` or `timeoutMs` in TypeScript, overrides the corresponding environment variable. +For example, passing `api_key` or `timeout_ms` in Python, `apiKey` or `timeoutMs` in TypeScript, or `ApiKey` or `TimeoutMs` in C# overrides the corresponding environment variable. `ORCHESTRATE_ADDITIONAL_HEADERS` is additive. It is merged into the request headers before the SDK applies its standard `Accept`, `Content-Type`, authentication, and metrics headers, so the SDK-managed headers take precedence if the same header name is supplied in multiple places. ### Examples -TypeScript: +#### TypeScript Example ```bash export ORCHESTRATE_API_KEY="your-api-key" @@ -91,7 +123,7 @@ import { OrchestrateApi } from '@careevolution/orchestrate'; const orchestrate = new OrchestrateApi(); ``` -Python: +#### Python Example ```bash export ORCHESTRATE_API_KEY="your-api-key" @@ -104,3 +136,18 @@ from orchestrate import OrchestrateApi api = OrchestrateApi() ``` + +#### C\# Example + +With environment values as above or DI configuration: + +```csharp +using CareEvolution.Orchestrate; + +var services = new ServiceCollection(); +services.AddOrchestrateApi(options => +{ + options.ApiKey = "your-api-key"; + options.TimeoutMs = 30000; +}); +``` diff --git a/dotnet/.config/dotnet-tools.json b/dotnet/.config/dotnet-tools.json new file mode 100644 index 0000000..8d74243 --- /dev/null +++ b/dotnet/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.6", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} diff --git a/dotnet/.csharpierignore b/dotnet/.csharpierignore new file mode 100644 index 0000000..db7fbb2 --- /dev/null +++ b/dotnet/.csharpierignore @@ -0,0 +1,3 @@ +**/bin +**/obj +tests/CareEvolution.Orchestrate.Tests/LiveData/** diff --git a/dotnet/.gitignore b/dotnet/.gitignore new file mode 100644 index 0000000..ae84b25 --- /dev/null +++ b/dotnet/.gitignore @@ -0,0 +1,3 @@ +**/bin/ +**/obj/ +TestResults/ diff --git a/dotnet/OrchestrateSDK.DotNet.sln b/dotnet/OrchestrateSDK.DotNet.sln new file mode 100644 index 0000000..260f374 --- /dev/null +++ b/dotnet/OrchestrateSDK.DotNet.sln @@ -0,0 +1,27 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareEvolution.Orchestrate", "src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj", "{B21EA726-0E99-4CDB-8F56-7A3300565A0B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareEvolution.Orchestrate.Tests", "tests/CareEvolution.Orchestrate.Tests/CareEvolution.Orchestrate.Tests.csproj", "{3259B89E-A60A-432D-A4B5-547646506A24}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B21EA726-0E99-4CDB-8F56-7A3300565A0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B21EA726-0E99-4CDB-8F56-7A3300565A0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B21EA726-0E99-4CDB-8F56-7A3300565A0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B21EA726-0E99-4CDB-8F56-7A3300565A0B}.Release|Any CPU.Build.0 = Release|Any CPU + {3259B89E-A60A-432D-A4B5-547646506A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3259B89E-A60A-432D-A4B5-547646506A24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3259B89E-A60A-432D-A4B5-547646506A24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3259B89E-A60A-432D-A4B5-547646506A24}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/dotnet/src/CareEvolution.Orchestrate/BatchRequest.cs b/dotnet/src/CareEvolution.Orchestrate/BatchRequest.cs new file mode 100644 index 0000000..f469bb3 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/BatchRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +internal sealed class BatchRequest +{ + public required IReadOnlyList Items { get; init; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/BatchResponse.cs b/dotnet/src/CareEvolution.Orchestrate/BatchResponse.cs new file mode 100644 index 0000000..cc3c0b5 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/BatchResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +internal sealed class BatchResponse +{ + public required List Items { get; init; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj b/dotnet/src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj new file mode 100644 index 0000000..3575f2b --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/CareEvolution.Orchestrate.csproj @@ -0,0 +1,26 @@ + + + net8.0;net10.0 + enable + enable + latest + true + $(NoWarn);1591 + CareEvolution.Orchestrate + CareEvolution Orchestrate SDK + SDK for the Orchestrate API at api.careevolutionapi.com + https://rosetta-api.docs.careevolution.com/ + https://github.com/CareEvolution/OrchestrateSDK + Apache-2.0 + CareEvolution + 0.0.0 + + + + + + + diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionRequest.cs new file mode 100644 index 0000000..4ebb636 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyConditionRequest +{ + public required string Code { get; set; } + + public required string System { get; set; } + + public string? Display { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionResponse.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionResponse.cs new file mode 100644 index 0000000..333ff91 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyConditionResponse.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyConditionResponse +{ + [JsonPropertyName("ccsrCatgory")] + public CodeableConcept? CcsrCategory { get; set; } + + public Coding? CcsrDefaultInpatient { get; set; } + + public Coding? CcsrDefaultOutpatient { get; set; } + + public bool CciChronic { get; set; } + + public bool CciAcute { get; set; } + + public CodeableConcept? HccCategory { get; set; } + + public bool Behavioral { get; set; } + + public bool Substance { get; set; } + + public bool SocialDeterminant { get; set; } + + public string? Covid19Condition { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationRequest.cs new file mode 100644 index 0000000..9486848 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyMedicationRequest +{ + public required string Code { get; set; } + + public required string System { get; set; } + + public string? Display { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationResponse.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationResponse.cs new file mode 100644 index 0000000..f109aac --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyMedicationResponse.cs @@ -0,0 +1,14 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyMedicationResponse +{ + public List MedRtTherapeuticClass { get; set; } = []; + + public List RxNormIngredient { get; set; } = []; + + public string? RxNormStrength { get; set; } + + public bool RxNormGeneric { get; set; } + + public string? Covid19Rx { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationRequest.cs new file mode 100644 index 0000000..9db9878 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyObservationRequest +{ + public required string Code { get; set; } + + public required string System { get; set; } + + public string? Display { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationResponse.cs b/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationResponse.cs new file mode 100644 index 0000000..aaaf10a --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ClassifyObservationResponse.cs @@ -0,0 +1,18 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ClassifyObservationResponse +{ + public string? LoincComponent { get; set; } + + public string? LoincClass { get; set; } + + public string? LoincSystem { get; set; } + + public string? LoincMethodType { get; set; } + + public string? LoincTimeAspect { get; set; } + + public string? Covid19Lab { get; set; } + + public string? Category { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs new file mode 100644 index 0000000..98171c0 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs @@ -0,0 +1,349 @@ +using System.Net.Http; + +namespace CareEvolution.Orchestrate; + +public interface IConvertApi +{ + Task CdaToFhirR4Async( + ConvertCdaToFhirR4Request request, + CancellationToken cancellationToken = default + ); + Task CdaToHtmlAsync( + ConvertCdaToHtmlRequest request, + CancellationToken cancellationToken = default + ); + Task CdaToPdfAsync( + ConvertCdaToPdfRequest request, + CancellationToken cancellationToken = default + ); + Task CombineFhirR4BundlesAsync( + ConvertCombineFhirR4BundlesRequest request, + CancellationToken cancellationToken = default + ); + Task FhirDstu2ToFhirR4Async( + ConvertFhirDstu2ToFhirR4Request request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToCdaAsync( + ConvertFhirR4ToCdaRequest request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToHealthLakeAsync( + ConvertFhirR4ToHealthLakeRequest request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToManifestAsync( + ConvertFhirR4ToManifestRequest request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToNemsisV34Async( + ConvertFhirR4ToNemsisV34Request request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToNemsisV35Async( + ConvertFhirR4ToNemsisV35Request request, + CancellationToken cancellationToken = default + ); + Task FhirR4ToOmopAsync( + ConvertFhirR4ToOmopRequest request, + CancellationToken cancellationToken = default + ); + Task FhirStu3ToFhirR4Async( + ConvertFhirStu3ToFhirR4Request request, + CancellationToken cancellationToken = default + ); + Task Hl7ToFhirR4Async( + ConvertHl7ToFhirR4Request request, + CancellationToken cancellationToken = default + ); + Task X12ToFhirR4Async( + ConvertX12ToFhirR4Request request, + CancellationToken cancellationToken = default + ); +} + +public sealed class ConvertApi : IConvertApi +{ + private readonly OrchestrateHttpClient _http; + + internal ConvertApi(OrchestrateHttpClient http) + { + _http = http; + } + + public Task Hl7ToFhirR4Async( + ConvertHl7ToFhirR4Request request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/convert/v1/hl7tofhirr4", + [ + new KeyValuePair("patientId", request.PatientId), + new KeyValuePair("patientIdentifier", request.PatientIdentifier), + new KeyValuePair( + "patientIdentifierSystem", + request.PatientIdentifierSystem + ), + new KeyValuePair("tz", request.Tz), + new KeyValuePair("processingHint", request.ProcessingHint), + ] + ); + return PostTextForJsonAsync( + route, + request.Content, + "text/plain", + cancellationToken + ); + } + + public Task CdaToFhirR4Async( + ConvertCdaToFhirR4Request request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/convert/v1/cdatofhirr4", + [ + new KeyValuePair("patientId", request.PatientId), + new KeyValuePair("patientIdentifier", request.PatientIdentifier), + new KeyValuePair( + "patientIdentifierSystem", + request.PatientIdentifierSystem + ), + new KeyValuePair( + "includeOriginalCda", + request.IncludeOriginalCda?.ToString()?.ToLowerInvariant() + ), + new KeyValuePair( + "includeStandardizedCda", + request.IncludeStandardizedCda?.ToString()?.ToLowerInvariant() + ), + ] + ); + return PostTextForJsonAsync( + route, + request.Content, + "application/xml", + cancellationToken + ); + } + + public Task CdaToPdfAsync( + ConvertCdaToPdfRequest request, + CancellationToken cancellationToken = default + ) => + PostTextAsync( + "/convert/v1/cdatopdf", + request.Content, + "application/xml", + "application/pdf", + ResponseKind.Bytes, + cancellationToken + ); + + public Task FhirR4ToCdaAsync( + ConvertFhirR4ToCdaRequest request, + CancellationToken cancellationToken = default + ) => + PostJsonAsync( + "/convert/v1/fhirr4tocda", + request.Content, + "application/xml", + ResponseKind.Text, + cancellationToken + ); + + public Task FhirR4ToOmopAsync( + ConvertFhirR4ToOmopRequest request, + CancellationToken cancellationToken = default + ) => + PostJsonAsync( + "/convert/v1/fhirr4toomop", + request.Content, + "application/zip", + ResponseKind.Bytes, + cancellationToken + ); + + public Task CombineFhirR4BundlesAsync( + ConvertCombineFhirR4BundlesRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/convert/v1/combinefhirr4bundles", + [ + new KeyValuePair("patientId", request.PatientId), + new KeyValuePair("patientIdentifier", request.PatientIdentifier), + new KeyValuePair( + "patientIdentifierSystem", + request.PatientIdentifierSystem + ), + ] + ); + return PostTextForJsonAsync( + route, + request.Content, + "application/x-ndjson", + cancellationToken + ); + } + + public Task X12ToFhirR4Async( + ConvertX12ToFhirR4Request request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/convert/v1/x12tofhirr4", + [ + new KeyValuePair("patientId", request.PatientId), + new KeyValuePair("patientIdentifier", request.PatientIdentifier), + new KeyValuePair( + "patientIdentifierSystem", + request.PatientIdentifierSystem + ), + ] + ); + return PostTextForJsonAsync( + route, + request.Content, + "text/plain", + cancellationToken + ); + } + + public Task FhirDstu2ToFhirR4Async( + ConvertFhirDstu2ToFhirR4Request request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/convert/v1/fhirdstu2tofhirr4", + request.Content, + cancellationToken + ); + + public Task FhirStu3ToFhirR4Async( + ConvertFhirStu3ToFhirR4Request request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/convert/v1/fhirstu3tofhirr4", + request.Content, + cancellationToken + ); + + public Task FhirR4ToHealthLakeAsync( + ConvertFhirR4ToHealthLakeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/convert/v1/fhirr4tohealthlake", + request.Content, + cancellationToken + ); + + public Task CdaToHtmlAsync( + ConvertCdaToHtmlRequest request, + CancellationToken cancellationToken = default + ) => + PostTextAsync( + "/convert/v1/cdatohtml", + request.Content, + "application/xml", + "text/html", + ResponseKind.Text, + cancellationToken + ); + + public Task FhirR4ToNemsisV34Async( + ConvertFhirR4ToNemsisV34Request request, + CancellationToken cancellationToken = default + ) => + PostJsonAsync( + "/convert/v1/fhirr4tonemsisv34", + request.Content, + "application/xml", + ResponseKind.Text, + cancellationToken + ); + + public Task FhirR4ToNemsisV35Async( + ConvertFhirR4ToNemsisV35Request request, + CancellationToken cancellationToken = default + ) => + PostJsonAsync( + "/convert/v1/fhirr4tonemsisv35", + request.Content, + "application/xml", + ResponseKind.Text, + cancellationToken + ); + + public Task FhirR4ToManifestAsync( + ConvertFhirR4ToManifestRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/convert/v1/fhirr4tomanifest", + [ + new KeyValuePair("delimiter", request.Delimiter), + new KeyValuePair("source", request.Source), + new KeyValuePair("patientIdentifier", request.PatientIdentifier), + new KeyValuePair("setting", request.Setting), + ] + ); + return PostJsonAsync( + route, + request.Content, + "application/zip", + ResponseKind.Bytes, + cancellationToken + ); + } + + private Task PostTextForJsonAsync( + string route, + string content, + string contentType, + CancellationToken cancellationToken + ) => + PostTextAsync( + route, + content, + contentType, + "application/json", + ResponseKind.Json, + cancellationToken + ); + + private Task PostTextAsync( + string route, + string content, + string contentType, + string accept, + ResponseKind responseKind, + CancellationToken cancellationToken + ) + { + using var httpContent = new StringContent(content); + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue( + contentType + ); + return _http.PostAsync(route, httpContent, accept, responseKind, cancellationToken); + } + + private Task PostJsonAsync( + string route, + Bundle content, + string accept, + ResponseKind responseKind, + CancellationToken cancellationToken + ) + { + var httpContent = OrchestrateHttpClient.CreateJsonContent(content); + return _http.PostAsync(route, httpContent, accept, responseKind, cancellationToken); + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToFhirR4Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToFhirR4Request.cs new file mode 100644 index 0000000..445b2e9 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToFhirR4Request.cs @@ -0,0 +1,16 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertCdaToFhirR4Request +{ + public required string Content { get; set; } + + public string? PatientId { get; set; } + + public string? PatientIdentifier { get; set; } + + public string? PatientIdentifierSystem { get; set; } + + public bool? IncludeOriginalCda { get; set; } + + public bool? IncludeStandardizedCda { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToHtmlRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToHtmlRequest.cs new file mode 100644 index 0000000..5a65ece --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToHtmlRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertCdaToHtmlRequest +{ + public required string Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToPdfRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToPdfRequest.cs new file mode 100644 index 0000000..353f841 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertCdaToPdfRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertCdaToPdfRequest +{ + public required string Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertCombineFhirR4BundlesRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertCombineFhirR4BundlesRequest.cs new file mode 100644 index 0000000..699bd46 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertCombineFhirR4BundlesRequest.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertCombineFhirR4BundlesRequest +{ + public required string Content { get; set; } + + public string? PatientId { get; set; } + + public string? PatientIdentifier { get; set; } + + public string? PatientIdentifierSystem { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirDstu2ToFhirR4Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirDstu2ToFhirR4Request.cs new file mode 100644 index 0000000..487d8f9 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirDstu2ToFhirR4Request.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirDstu2ToFhirR4Request +{ + public required object Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToCdaRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToCdaRequest.cs new file mode 100644 index 0000000..6651f87 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToCdaRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToCdaRequest +{ + public required Bundle Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToHealthLakeRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToHealthLakeRequest.cs new file mode 100644 index 0000000..66d24f0 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToHealthLakeRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToHealthLakeRequest +{ + public required Bundle Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToManifestRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToManifestRequest.cs new file mode 100644 index 0000000..c8a2ac4 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToManifestRequest.cs @@ -0,0 +1,14 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToManifestRequest +{ + public required Bundle Content { get; set; } + + public string? Delimiter { get; set; } + + public string? Source { get; set; } + + public string? PatientIdentifier { get; set; } + + public string? Setting { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV34Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV34Request.cs new file mode 100644 index 0000000..e7d8db7 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV34Request.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToNemsisV34Request +{ + public required Bundle Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV35Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV35Request.cs new file mode 100644 index 0000000..cbeca66 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToNemsisV35Request.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToNemsisV35Request +{ + public required Bundle Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToOmopRequest.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToOmopRequest.cs new file mode 100644 index 0000000..62ab2f7 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirR4ToOmopRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirR4ToOmopRequest +{ + public required Bundle Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertFhirStu3ToFhirR4Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirStu3ToFhirR4Request.cs new file mode 100644 index 0000000..6f6b3b0 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertFhirStu3ToFhirR4Request.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertFhirStu3ToFhirR4Request +{ + public required object Content { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertHl7ToFhirR4Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertHl7ToFhirR4Request.cs new file mode 100644 index 0000000..a5bfe74 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertHl7ToFhirR4Request.cs @@ -0,0 +1,16 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertHl7ToFhirR4Request +{ + public required string Content { get; set; } + + public string? PatientId { get; set; } + + public string? PatientIdentifier { get; set; } + + public string? PatientIdentifierSystem { get; set; } + + public string? Tz { get; set; } + + public string? ProcessingHint { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertRequestFactory.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertRequestFactory.cs new file mode 100644 index 0000000..9e6acad --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertRequestFactory.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CareEvolution.Orchestrate; + +public static class ConvertRequestFactory +{ + public static ConvertCombineFhirR4BundlesRequest GenerateConvertCombinedFhirBundlesRequestFromBundles( + IEnumerable fhirBundles, + string? personId = null + ) + { + var content = string.Join("\n", fhirBundles.Select(OrchestrateHttpClient.Serialize)); + + return new ConvertCombineFhirR4BundlesRequest { Content = content, PatientId = personId }; + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertX12ToFhirR4Request.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertX12ToFhirR4Request.cs new file mode 100644 index 0000000..badcba5 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertX12ToFhirR4Request.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class ConvertX12ToFhirR4Request +{ + public required string Content { get; set; } + + public string? PatientId { get; set; } + + public string? PatientIdentifier { get; set; } + + public string? PatientIdentifierSystem { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs new file mode 100644 index 0000000..c768862 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -0,0 +1,121 @@ +using System.Text.Json; + +namespace CareEvolution.Orchestrate; + +internal static class EnvironmentConfiguration +{ + private const string DefaultBaseUrl = "https://api.careevolutionapi.com"; + private const int DefaultTimeoutMs = 120_000; + private const string BaseUrlEnvironmentVariable = "ORCHESTRATE_BASE_URL"; + private const string IdentityUrlEnvironmentVariable = "ORCHESTRATE_IDENTITY_URL"; + private const string LocalHashingUrlEnvironmentVariable = + "ORCHESTRATE_IDENTITY_LOCAL_HASHING_URL"; + private const string AdditionalHeadersEnvironmentVariable = "ORCHESTRATE_ADDITIONAL_HEADERS"; + private const string ApiKeyEnvironmentVariable = "ORCHESTRATE_API_KEY"; + private const string IdentityApiKeyEnvironmentVariable = "ORCHESTRATE_IDENTITY_API_KEY"; + private const string IdentityMetricsKeyEnvironmentVariable = "ORCHESTRATE_IDENTITY_METRICS_KEY"; + private const string TimeoutEnvironmentVariable = "ORCHESTRATE_TIMEOUT_MS"; + + public static ResolvedConfiguration Resolve(OrchestrateClientOptions? options) + { + return new ResolvedConfiguration( + BaseUrl: GetPriority(options?.BaseUrl, BaseUrlEnvironmentVariable) ?? DefaultBaseUrl, + TimeoutMs: GetTimeout(options?.TimeoutMs), + AdditionalHeaders: GetAdditionalHeaders(), + ApiKey: GetPriority(options?.ApiKey, ApiKeyEnvironmentVariable) + ); + } + + public static ResolvedConfiguration Resolve(IdentityApiOptions? options) + { + var url = GetPriority(options?.Url, IdentityUrlEnvironmentVariable); + if (string.IsNullOrWhiteSpace(url)) + { + throw new ArgumentException( + $"Identity URL is required. Specify in the constructor or set '{IdentityUrlEnvironmentVariable}' environment variable." + ); + } + + var metricsKey = GetPriority(options?.MetricsKey, IdentityMetricsKeyEnvironmentVariable); + return new ResolvedConfiguration( + BaseUrl: url, + TimeoutMs: GetTimeout(options?.TimeoutMs), + AdditionalHeaders: GetAdditionalHeaders(), + ApiKey: GetPriority(options?.ApiKey, IdentityApiKeyEnvironmentVariable), + Authorization: string.IsNullOrWhiteSpace(metricsKey) + ? null + : $"Basic {metricsKey.Replace("Basic ", string.Empty, StringComparison.Ordinal)}" + ); + } + + public static ResolvedConfiguration Resolve(LocalHashingApiOptions? options) + { + var url = GetPriority(options?.Url, LocalHashingUrlEnvironmentVariable); + if (string.IsNullOrWhiteSpace(url)) + { + throw new ArgumentException( + $"Local hashing URL is required. Specify in the constructor or set '{LocalHashingUrlEnvironmentVariable}' environment variable." + ); + } + + return new ResolvedConfiguration( + BaseUrl: url, + TimeoutMs: GetTimeout(options?.TimeoutMs), + AdditionalHeaders: GetAdditionalHeaders() + ); + } + + private static string? GetPriority(string? explicitValue, string environmentVariable) + { + return explicitValue ?? Environment.GetEnvironmentVariable(environmentVariable); + } + + private static int GetTimeout(int? explicitTimeoutMs) + { + var rawValue = + explicitTimeoutMs?.ToString() + ?? Environment.GetEnvironmentVariable(TimeoutEnvironmentVariable); + if (rawValue is null) + { + return DefaultTimeoutMs; + } + + if (!int.TryParse(rawValue, out var timeoutMs)) + { + throw new ArgumentException( + $"Invalid timeout value in environment variable '{TimeoutEnvironmentVariable}': {rawValue}" + ); + } + + return timeoutMs; + } + + private static IReadOnlyDictionary GetAdditionalHeaders() + { + var rawValue = Environment.GetEnvironmentVariable(AdditionalHeadersEnvironmentVariable); + if (string.IsNullOrWhiteSpace(rawValue)) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + using var document = JsonDocument.Parse(rawValue); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + throw new ArgumentException( + $"Environment variable '{AdditionalHeadersEnvironmentVariable}' must be a JSON object." + ); + } + + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var property in document.RootElement.EnumerateObject()) + { + headers[property.Name] = + property.Value.GetString() + ?? throw new ArgumentException( + $"Environment variable '{AdditionalHeadersEnvironmentVariable}' must map header names to string values." + ); + } + + return headers; + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs new file mode 100644 index 0000000..32ed09b --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs @@ -0,0 +1,18 @@ +using System.Net; + +namespace CareEvolution.Orchestrate.Exceptions; + +public sealed class OrchestrateClientError( + string responseText, + IReadOnlyList issues, + HttpStatusCode statusCode +) + : OrchestrateHttpError( + issues.Count > 0 ? $"\n * {string.Join(" \n * ", issues)}" : responseText, + statusCode + ) +{ + public string ResponseText { get; } = responseText; + + public IReadOnlyList Issues { get; } = issues; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs new file mode 100644 index 0000000..4805b56 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs @@ -0,0 +1,3 @@ +namespace CareEvolution.Orchestrate.Exceptions; + +public class OrchestrateError(string message) : Exception(message) { } diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs new file mode 100644 index 0000000..716cd55 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace CareEvolution.Orchestrate.Exceptions; + +public class OrchestrateHttpError(string message, HttpStatusCode? statusCode = null) + : OrchestrateError(message) +{ + public HttpStatusCode? StatusCode { get; } = statusCode; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/GetFhirR4CodeSystemRequest.cs b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4CodeSystemRequest.cs new file mode 100644 index 0000000..1f7ddf7 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4CodeSystemRequest.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class GetFhirR4CodeSystemRequest +{ + public required string CodeSystem { get; set; } + + public int? PageNumber { get; set; } + + public int? PageSize { get; set; } + + public string? ConceptContains { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetRequest.cs b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetRequest.cs new file mode 100644 index 0000000..3791ee4 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class GetFhirR4ValueSetRequest +{ + public required string Id { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetsByScopeRequest.cs b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetsByScopeRequest.cs new file mode 100644 index 0000000..4e8481f --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/GetFhirR4ValueSetsByScopeRequest.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class GetFhirR4ValueSetsByScopeRequest +{ + public string? Name { get; set; } + + public int? PageNumber { get; set; } + + public int? PageSize { get; set; } + + public string? Scope { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/GlobalUsings.cs b/dotnet/src/CareEvolution.Orchestrate/GlobalUsings.cs new file mode 100644 index 0000000..dd27951 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using CareEvolution.Orchestrate.Identity; +global using Bundle = Hl7.Fhir.Model.R4.Bundle; +global using CodeableConcept = Hl7.Fhir.Model.CodeableConcept; +global using CodeSystem = Hl7.Fhir.Model.R4.CodeSystem; +global using Coding = Hl7.Fhir.Model.Coding; +global using ConceptMap = Hl7.Fhir.Model.R4.ConceptMap; +global using Parameters = Hl7.Fhir.Model.Parameters; +global using ValueSet = Hl7.Fhir.Model.R4.ValueSet; diff --git a/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs new file mode 100644 index 0000000..c4a9893 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; + +namespace CareEvolution.Orchestrate; + +[EditorBrowsable(EditorBrowsableState.Advanced)] +public interface IOrchestrateHttpClient : IDisposable +{ + HttpClient HttpClient { get; } + + Task SendAsync( + HttpMethod method, + string path, + HttpContent? content = null, + string accept = "application/json", + CancellationToken cancellationToken = default + ); + + Task GetJsonAsync(string path, CancellationToken cancellationToken = default); + + Task PostJsonAsync( + string path, + object? body, + CancellationToken cancellationToken = default + ); +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceRequest.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceRequest.cs new file mode 100644 index 0000000..800bbbf --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class AddMatchGuidanceRequest : MatchGuidanceRequest +{ + public required string Action { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceResponse.cs new file mode 100644 index 0000000..4544f22 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/AddMatchGuidanceResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class AddMatchGuidanceResponse +{ + public List ChangedPersons { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateBlindedRecordRequest.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateBlindedRecordRequest.cs new file mode 100644 index 0000000..2186cfc --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateBlindedRecordRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class AddOrUpdateBlindedRecordRequest +{ + public required string Source { get; set; } + + public required string Identifier { get; set; } + + public required BlindedDemographic BlindedDemographic { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordRequest.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordRequest.cs new file mode 100644 index 0000000..663fa68 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class AddOrUpdateRecordRequest +{ + public required string Source { get; set; } + + public required string Identifier { get; set; } + + public required Demographic Demographic { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordResponse.cs new file mode 100644 index 0000000..d85d957 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/AddOrUpdateRecordResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class AddOrUpdateRecordResponse : MatchedPersonReference +{ + public Advisories? Advisories { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/Advisories.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/Advisories.cs new file mode 100644 index 0000000..1cd4902 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/Advisories.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class Advisories +{ + public List InvalidDemographicFields { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/BlindedDemographic.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/BlindedDemographic.cs new file mode 100644 index 0000000..f5b52c2 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/BlindedDemographic.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate.Identity; + +public class BlindedDemographic +{ + public required string Data { get; set; } + + public int Version { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/DatasourceOverlapRecord.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/DatasourceOverlapRecord.cs new file mode 100644 index 0000000..6f759f3 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/DatasourceOverlapRecord.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class DatasourceOverlapRecord +{ + public string? DatasourceA { get; set; } + + public string? DatasourceB { get; set; } + + public int OverlapCount { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/DeleteRecordResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/DeleteRecordResponse.cs new file mode 100644 index 0000000..5e06787 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/DeleteRecordResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class DeleteRecordResponse +{ + public List ChangedPersons { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/Demographic.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/Demographic.cs new file mode 100644 index 0000000..1a6736b --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/Demographic.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace CareEvolution.Orchestrate.Identity; + +public sealed class Demographic +{ + public string? FirstName { get; set; } + + public string? MiddleName { get; set; } + + public string? LastName { get; set; } + + public string? MaidenName { get; set; } + + public string? Gender { get; set; } + + public string? Race { get; set; } + + public string? HomePhoneNumber { get; set; } + + public string? CellPhoneNumber { get; set; } + + public string? Email { get; set; } + + public string? Dob { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? ZipCode { get; set; } + + public string? Mrn { get; set; } + + public string? Hcid { get; set; } + + public string? Ssn { get; set; } + + [JsonPropertyName("medicaidID")] + public string? MedicaidId { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/GetPersonByIdRequest.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/GetPersonByIdRequest.cs new file mode 100644 index 0000000..bfabc0e --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/GetPersonByIdRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class GetPersonByIdRequest +{ + public required string Id { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/HashDemographicResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/HashDemographicResponse.cs new file mode 100644 index 0000000..7771ec1 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/HashDemographicResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class HashDemographicResponse : BlindedDemographic +{ + public Advisories? Advisories { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsRecord.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsRecord.cs new file mode 100644 index 0000000..eafc29c --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsRecord.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class IdentifierMetricsRecord +{ + public string? IdentifierType { get; set; } + + public int RecordCount { get; set; } + + public double RecordRatio { get; set; } + + public string? Source { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsResponse.cs new file mode 100644 index 0000000..ece8d6d --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentifierMetricsResponse.cs @@ -0,0 +1,16 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class IdentifierMetricsResponse +{ + public string? Refreshed { get; set; } + + public int TotalRecordCount { get; set; } + + public int TotalPersonCount { get; set; } + + public List GlobalMetricsRecords { get; set; } = []; + + public List SummaryMetricsRecords { get; set; } = []; + + public List SourceTotals { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs new file mode 100644 index 0000000..f3b3e6a --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs @@ -0,0 +1,113 @@ +using System.ComponentModel; + +namespace CareEvolution.Orchestrate.Identity; + +public sealed class IdentityApi : IDisposable +{ + private readonly OrchestrateHttpClient _http; + + public IdentityApi(IdentityApiOptions? options = null) + : this(httpClient: null, options) { } + + public IdentityApi(HttpClient? httpClient, IdentityApiOptions? options = null) + { + _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); + Monitoring = new IdentityMonitoringApi(_http); + } + + public IdentityMonitoringApi Monitoring { get; } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IOrchestrateHttpClient Transport => _http; + + public HttpClient HttpClient => _http.HttpClient; + + public Task AddOrUpdateRecordAsync( + AddOrUpdateRecordRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + $"/mpi/v1/record/{BuildSourceIdentifierRoute(request.Source, request.Identifier)}", + request.Demographic, + cancellationToken + ); + + public Task AddOrUpdateBlindedRecordAsync( + AddOrUpdateBlindedRecordRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + $"/mpi/v1/blindedRecord/{BuildSourceIdentifierRoute(request.Source, request.Identifier)}", + request.BlindedDemographic, + cancellationToken + ); + + public Task GetPersonByRecordAsync( + Record request, + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + $"/mpi/v1/record/{BuildSourceIdentifierRoute(request.Source, request.Identifier)}", + cancellationToken + ); + + public Task GetPersonByIdAsync( + GetPersonByIdRequest request, + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + $"/mpi/v1/person/{RouteBuilder.Escape(request.Id)}", + cancellationToken + ); + + public Task MatchDemographicsAsync( + Demographic request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync("/mpi/v1/match", request, cancellationToken); + + public Task MatchBlindedDemographicsAsync( + BlindedDemographic request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/mpi/v1/matchBlinded", + request, + cancellationToken + ); + + public Task DeleteRecordAsync( + Record request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + $"/mpi/v1/deleteRecord/{BuildSourceIdentifierRoute(request.Source, request.Identifier)}", + new { }, + cancellationToken + ); + + public Task AddMatchGuidanceAsync( + AddMatchGuidanceRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/mpi/v1/addGuidance", + request, + cancellationToken + ); + + public Task RemoveMatchGuidanceAsync( + MatchGuidanceRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/mpi/v1/removeGuidance", + request, + cancellationToken + ); + + public void Dispose() => _http.Dispose(); + + private static string BuildSourceIdentifierRoute(string source, string identifier) => + $"{RouteBuilder.Escape(source)}/{RouteBuilder.Escape(identifier)}"; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityMonitoringApi.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityMonitoringApi.cs new file mode 100644 index 0000000..fba133c --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityMonitoringApi.cs @@ -0,0 +1,27 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class IdentityMonitoringApi +{ + private readonly OrchestrateHttpClient _http; + + internal IdentityMonitoringApi(OrchestrateHttpClient http) + { + _http = http; + } + + public Task IdentifierMetricsAsync( + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + "/monitoring/v1/identifierMetrics", + cancellationToken + ); + + public Task OverlapMetricsAsync( + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + "/monitoring/v1/overlapMetrics", + cancellationToken + ); +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs new file mode 100644 index 0000000..78201b9 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; + +namespace CareEvolution.Orchestrate.Identity; + +public sealed class LocalHashingApi : IDisposable +{ + private readonly OrchestrateHttpClient _http; + + public LocalHashingApi(LocalHashingApiOptions? options = null) + : this(httpClient: null, options) { } + + public LocalHashingApi(HttpClient? httpClient, LocalHashingApiOptions? options = null) + { + _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); + } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IOrchestrateHttpClient Transport => _http; + + public HttpClient HttpClient => _http.HttpClient; + + public Task HashAsync( + Demographic demographic, + CancellationToken cancellationToken = default + ) => _http.PostJsonAsync("/hash", demographic, cancellationToken); + + public void Dispose() => _http.Dispose(); +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/MatchBlindedDemographicsResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchBlindedDemographicsResponse.cs new file mode 100644 index 0000000..9b198b8 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchBlindedDemographicsResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace CareEvolution.Orchestrate.Identity; + +public sealed class MatchBlindedDemographicsResponse +{ + [JsonPropertyName("matchingPersons")] + public List MatchingPersons { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/MatchDemographicsResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchDemographicsResponse.cs new file mode 100644 index 0000000..fbbcb34 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchDemographicsResponse.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace CareEvolution.Orchestrate.Identity; + +public sealed class MatchDemographicsResponse +{ + [JsonPropertyName("matchingPersons")] + public List MatchingPersons { get; set; } = []; + + public List Advisories { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/MatchGuidanceRequest.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchGuidanceRequest.cs new file mode 100644 index 0000000..15d6889 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchGuidanceRequest.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate.Identity; + +public class MatchGuidanceRequest +{ + public required Record RecordOne { get; set; } + + public required Record RecordTwo { get; set; } + + public required string Comment { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/MatchedPersonReference.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchedPersonReference.cs new file mode 100644 index 0000000..2f3f53c --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/MatchedPersonReference.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate.Identity; + +public class MatchedPersonReference +{ + public Person? MatchedPerson { get; set; } + + public List ChangedPersons { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/OverlapMetricsResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/OverlapMetricsResponse.cs new file mode 100644 index 0000000..4da309d --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/OverlapMetricsResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class OverlapMetricsResponse +{ + public List DatasourceOverlapRecords { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/Person.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/Person.cs new file mode 100644 index 0000000..b771e98 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/Person.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class Person +{ + public required string Id { get; set; } + + public List Records { get; set; } = []; + + public int Version { get; set; } + + public PersonStatus? Status { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/PersonStatus.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/PersonStatus.cs new file mode 100644 index 0000000..7b5147b --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/PersonStatus.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class PersonStatus +{ + public string? Code { get; set; } + + public List SupersededBy { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/Record.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/Record.cs new file mode 100644 index 0000000..18f30af --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/Record.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class Record +{ + public required string Source { get; set; } + + public required string Identifier { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/RemoveMatchGuidanceResponse.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/RemoveMatchGuidanceResponse.cs new file mode 100644 index 0000000..e2bab13 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/RemoveMatchGuidanceResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class RemoveMatchGuidanceResponse +{ + public List ChangedPersons { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/SourceTotal.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/SourceTotal.cs new file mode 100644 index 0000000..ff1ade9 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/SourceTotal.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate.Identity; + +public sealed class SourceTotal +{ + public string? Source { get; set; } + + public int TotalRecordCount { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/InsightApi.cs b/dotnet/src/CareEvolution.Orchestrate/InsightApi.cs new file mode 100644 index 0000000..2634596 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/InsightApi.cs @@ -0,0 +1,35 @@ +namespace CareEvolution.Orchestrate; + +public interface IInsightApi +{ + Task RiskProfileAsync( + InsightRiskProfileRequest request, + CancellationToken cancellationToken = default + ); +} + +public sealed class InsightApi : IInsightApi +{ + private readonly OrchestrateHttpClient _http; + + internal InsightApi(OrchestrateHttpClient http) + { + _http = http; + } + + public Task RiskProfileAsync( + InsightRiskProfileRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/insight/v1/riskprofile", + [ + new KeyValuePair("hcc_version", request.HccVersion), + new KeyValuePair("period_end_date", request.PeriodEndDate), + new KeyValuePair("ra_segment", request.RaSegment), + ] + ); + return _http.PostJsonAsync(route, request.Content, cancellationToken); + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/InsightRiskProfileRequest.cs b/dotnet/src/CareEvolution.Orchestrate/InsightRiskProfileRequest.cs new file mode 100644 index 0000000..523eaa2 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/InsightRiskProfileRequest.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class InsightRiskProfileRequest +{ + public required Bundle Content { get; set; } + + public string? HccVersion { get; set; } + + public string? PeriodEndDate { get; set; } + + public string? RaSegment { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Options.cs b/dotnet/src/CareEvolution.Orchestrate/Options.cs new file mode 100644 index 0000000..ebf59c2 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Options.cs @@ -0,0 +1,28 @@ +namespace CareEvolution.Orchestrate; + +public class OrchestrateClientOptions +{ + public string? ApiKey { get; set; } + + public string? BaseUrl { get; set; } + + public int? TimeoutMs { get; set; } +} + +public class IdentityApiOptions +{ + public string? Url { get; set; } + + public string? ApiKey { get; set; } + + public string? MetricsKey { get; set; } + + public int? TimeoutMs { get; set; } +} + +public class LocalHashingApiOptions +{ + public string? Url { get; set; } + + public int? TimeoutMs { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs new file mode 100644 index 0000000..6ba0cc3 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; + +namespace CareEvolution.Orchestrate; + +public interface IOrchestrateApi : IDisposable +{ + ITerminologyApi Terminology { get; } + IConvertApi Convert { get; } + IInsightApi Insight { get; } + IOrchestrateHttpClient HttpHandler { get; } +} + +public sealed class OrchestrateApi : IOrchestrateApi +{ + private readonly OrchestrateHttpClient _http; + + public OrchestrateApi(OrchestrateClientOptions? options = null) + : this(httpClient: null, options) { } + + public OrchestrateApi(HttpClient? httpClient, OrchestrateClientOptions? options = null) + { + _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); + Terminology = new TerminologyApi(_http); + Convert = new ConvertApi(_http); + Insight = new InsightApi(_http); + } + + public ITerminologyApi Terminology { get; } + + public IConvertApi Convert { get; } + + public IInsightApi Insight { get; } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IOrchestrateHttpClient HttpHandler => _http; + + public void Dispose() => _http.Dispose(); +} diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs new file mode 100644 index 0000000..1cd85bc --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -0,0 +1,452 @@ +using System.ComponentModel; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using CareEvolution.Orchestrate.Exceptions; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; + +namespace CareEvolution.Orchestrate; + +internal sealed class OrchestrateHttpClient( + ResolvedConfiguration configuration, + HttpClient? httpClient = null +) : IOrchestrateHttpClient +{ + private static readonly FhirJsonFastParser FhirJsonParser = CreateFhirJsonParser(); + private static readonly FhirJsonFastSerializer FhirJsonSerializer = CreateFhirJsonSerializer(); + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, + }; + + private readonly HttpClient _httpClient = httpClient ?? new HttpClient(); + private readonly bool _disposeHttpClient = httpClient is null; + private readonly ResolvedConfiguration _configuration = configuration; + + public HttpClient HttpClient => _httpClient; + + public async Task GetJsonAsync(string path, CancellationToken cancellationToken = default) + { + var responseText = await SendForStringAsync( + HttpMethod.Get, + path, + content: null, + accept: "application/json", + cancellationToken + ) + .ConfigureAwait(false); + return Deserialize(responseText); + } + + public async Task PostJsonAsync( + string path, + object? body, + CancellationToken cancellationToken = default + ) + { + using var content = CreateJsonContent(body); + var responseText = await SendForStringAsync( + HttpMethod.Post, + path, + content, + accept: "application/json", + cancellationToken + ) + .ConfigureAwait(false); + return Deserialize(responseText); + } + + public async Task PostAsync( + string path, + HttpContent content, + string accept, + ResponseKind responseKind, + CancellationToken cancellationToken = default + ) + { + return responseKind switch + { + ResponseKind.Json => Deserialize( + await SendForStringAsync(HttpMethod.Post, path, content, accept, cancellationToken) + .ConfigureAwait(false) + ), + ResponseKind.Text => (T) + (object) + await SendForStringAsync( + HttpMethod.Post, + path, + content, + accept, + cancellationToken + ) + .ConfigureAwait(false), + ResponseKind.Bytes => (T) + (object) + await SendForBytesAsync( + HttpMethod.Post, + path, + content, + accept, + cancellationToken + ) + .ConfigureAwait(false), + _ => throw new ArgumentOutOfRangeException(nameof(responseKind)), + }; + } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Task SendAsync( + HttpMethod method, + string path, + HttpContent? content = null, + string accept = "application/json", + CancellationToken cancellationToken = default + ) + { + if (typeof(T) == typeof(byte[])) + { + return SendBytesAsync(method, path, content, accept, cancellationToken); + } + + if (typeof(T) == typeof(string)) + { + return SendTextAsync(method, path, content, accept, cancellationToken); + } + + return SendJsonAsync(method, path, content, accept, cancellationToken); + } + + internal static HttpContent CreateJsonContent(object? body) + { + var payload = Serialize(body); + return new StringContent(payload, Encoding.UTF8, "application/json"); + } + + internal static string Serialize(object? body) + { + if (body is null) + { + return "null"; + } + + if (TrySerializeFhir(body) is { } fhirJson) + { + return fhirJson; + } + + return JsonSerializer.Serialize(body, JsonOptions); + } + + private static T Deserialize(string responseText) + { + if (TryDeserializeFhir(responseText) is { } fhirValue) + { + return fhirValue; + } + + return JsonSerializer.Deserialize(responseText, JsonOptions) + ?? throw new InvalidOperationException( + $"Unable to deserialize response as {typeof(T).Name}." + ); + } + + private static string? TrySerializeFhir(object body) + { + if (body is not Hl7.Fhir.Model.Base fhirValue) + { + return null; + } + + return FhirJsonSerializer.SerializeToString(fhirValue, SummaryType.False, null); + } + + private static T? TryDeserializeFhir(string responseText) + { + if ( + !IsFhirType(typeof(T)) + || !responseText.Contains("\"resourceType\"", StringComparison.Ordinal) + ) + { + return default; + } + + return (T?)(object?)FhirJsonParser.Parse(responseText, typeof(T)); + } + + private static bool IsFhirType(Type type) => + type.Namespace?.StartsWith("Hl7.Fhir.Model", StringComparison.Ordinal) == true; + + private static FhirJsonFastParser CreateFhirJsonParser() + { + var settings = new ParserSettings(Hl7.Fhir.Model.Version.R4) + { + AllowUnrecognizedEnums = true, + AcceptUnknownMembers = true, + PermissiveParsing = true, + }; + return new FhirJsonFastParser(settings); + } + + private static FhirJsonFastSerializer CreateFhirJsonSerializer() + { + return new FhirJsonFastSerializer(new SerializerSettings(Hl7.Fhir.Model.Version.R4), false); + } + + private async Task SendForStringAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) + { + using var response = await SendAsync(method, path, content, accept, cancellationToken) + .ConfigureAwait(false); + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task SendForBytesAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) + { + using var response = await SendAsync(method, path, content, accept, cancellationToken) + .ConfigureAwait(false); + return await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task SendJsonAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) + { + var responseText = await SendForStringAsync( + method, + path, + content, + accept, + cancellationToken + ) + .ConfigureAwait(false); + return Deserialize(responseText); + } + + private async Task SendTextAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) => + (T) + (object) + await SendForStringAsync(method, path, content, accept, cancellationToken) + .ConfigureAwait(false); + + private async Task SendBytesAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) => + (T) + (object) + await SendForBytesAsync(method, path, content, accept, cancellationToken) + .ConfigureAwait(false); + + private async Task SendAsync( + HttpMethod method, + string path, + HttpContent? content, + string accept, + CancellationToken cancellationToken + ) + { + using var cts = + _configuration.TimeoutMs > 0 + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) + : null; + cts?.CancelAfter(_configuration.TimeoutMs); + var effectiveCancellationToken = cts?.Token ?? cancellationToken; + + using var request = new HttpRequestMessage(method, BuildUri(path)) { Content = content }; + + ApplyHeaders(request.Headers, accept); + + var response = await _httpClient + .SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + effectiveCancellationToken + ) + .ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + return response; + } + + throw await CreateExceptionAsync(response, effectiveCancellationToken) + .ConfigureAwait(false); + } + + private Uri BuildUri(string path) + { + var normalizedBaseUrl = _configuration.BaseUrl.TrimEnd('/'); + var normalizedPath = path.StartsWith("/", StringComparison.Ordinal) ? path : $"/{path}"; + return new Uri($"{normalizedBaseUrl}{normalizedPath}", UriKind.Absolute); + } + + private void ApplyHeaders(HttpRequestHeaders headers, string accept) + { + foreach (var header in _configuration.AdditionalHeaders) + { + headers.TryAddWithoutValidation(header.Key, header.Value); + } + + if (!string.IsNullOrWhiteSpace(_configuration.ApiKey)) + { + headers.Remove("x-api-key"); + headers.TryAddWithoutValidation("x-api-key", _configuration.ApiKey); + } + + if (!string.IsNullOrWhiteSpace(_configuration.Authorization)) + { + headers.Remove("Authorization"); + headers.TryAddWithoutValidation("Authorization", _configuration.Authorization); + } + + headers.Accept.Clear(); + headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept)); + } + + private static async Task CreateExceptionAsync( + HttpResponseMessage response, + CancellationToken cancellationToken + ) + { + var responseText = await response + .Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + var issues = ReadOperationalOutcomes(responseText); + if ((int)response.StatusCode >= 400 && (int)response.StatusCode < 600) + { + return new OrchestrateClientError(responseText, issues, response.StatusCode); + } + + return new OrchestrateHttpError(responseText, response.StatusCode); + } + + private static IReadOnlyList ReadOperationalOutcomes(string responseText) + { + var outcomes = ReadJsonOutcomes(responseText); + return outcomes.Count > 0 ? outcomes : [responseText]; + } + + private static List ReadJsonOutcomes(string responseText) + { + try + { + var root = JsonNode.Parse(responseText)?.AsObject(); + if (root is null) + { + return []; + } + + if ( + root.TryGetPropertyValue("issue", out var issueNode) + && issueNode is JsonArray issuesArray + ) + { + return issuesArray + .OfType() + .Select(issue => + { + var severity = issue["severity"]?.GetValue() ?? string.Empty; + var code = issue["code"]?.GetValue() ?? string.Empty; + var diagnostics = issue["diagnostics"]?.GetValue() ?? string.Empty; + var details = GetIssueDetailString(issue["details"]); + var prefix = $"{severity}: {code}"; + var message = string.Join( + "; ", + new[] { details, diagnostics }.Where(static value => + !string.IsNullOrWhiteSpace(value) + ) + ); + return string.IsNullOrWhiteSpace(message) + ? prefix + : $"{prefix} - {message}"; + }) + .ToList(); + } + + if ( + (root["type"]?.GetValue() ?? string.Empty) + == "https://tools.ietf.org/html/rfc9110#section-15.5.1" + ) + { + var title = root["title"]?.GetValue() ?? string.Empty; + var detail = root["detail"]?.GetValue() ?? string.Empty; + return [$"error: {title} - {detail}".TrimEnd()]; + } + } + catch + { + return []; + } + + return []; + } + + private static string GetIssueDetailString(JsonNode? detailNode) + { + if (detailNode is not JsonObject detailObject) + { + return string.Empty; + } + + if (detailObject["text"]?.GetValue() is { Length: > 0 } text) + { + return text; + } + + if (detailObject["coding"] is JsonArray codingArray) + { + foreach (var codingNode in codingArray.OfType()) + { + var code = codingNode["code"]?.GetValue(); + var display = codingNode["display"]?.GetValue(); + var joined = string.Join( + ": ", + new[] { code, display }.Where(static value => !string.IsNullOrWhiteSpace(value)) + ); + if (!string.IsNullOrWhiteSpace(joined)) + { + return joined; + } + } + } + + return string.Empty; + } + + public void Dispose() + { + if (_disposeHttpClient) + { + _httpClient.Dispose(); + } + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/Registration.cs b/dotnet/src/CareEvolution.Orchestrate/Registration.cs new file mode 100644 index 0000000..5347303 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Registration.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace CareEvolution.Orchestrate; + +public static class Registration +{ + public static IServiceCollection AddOrchestrateApi(this IServiceCollection services) => + services.AddOrchestrateApi(configure: null); + + public static IServiceCollection AddOrchestrateApi( + this IServiceCollection services, + Action? configure + ) + { + services.AddOptions(); + + if (configure is not null) + { + services.Configure(configure); + } + + services.AddHttpClient(nameof(OrchestrateApi)); + services.AddTransient(serviceProvider => new OrchestrateApi( + serviceProvider + .GetRequiredService() + .CreateClient(nameof(OrchestrateApi)), + serviceProvider.GetRequiredService>().Value + )); + + return services; + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/ResolvedConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/ResolvedConfiguration.cs new file mode 100644 index 0000000..3dd9fb5 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ResolvedConfiguration.cs @@ -0,0 +1,9 @@ +namespace CareEvolution.Orchestrate; + +internal sealed record ResolvedConfiguration( + string BaseUrl, + int TimeoutMs, + IReadOnlyDictionary AdditionalHeaders, + string? ApiKey = null, + string? Authorization = null +); diff --git a/dotnet/src/CareEvolution.Orchestrate/ResponseKind.cs b/dotnet/src/CareEvolution.Orchestrate/ResponseKind.cs new file mode 100644 index 0000000..f90fc50 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/ResponseKind.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate; + +internal enum ResponseKind +{ + Json, + Text, + Bytes, +} diff --git a/dotnet/src/CareEvolution.Orchestrate/RouteBuilder.cs b/dotnet/src/CareEvolution.Orchestrate/RouteBuilder.cs new file mode 100644 index 0000000..75dfddb --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/RouteBuilder.cs @@ -0,0 +1,29 @@ +using Hl7.Fhir.Rest; + +namespace CareEvolution.Orchestrate; + +internal static class RouteBuilder +{ + public static string Build( + string path, + params IEnumerable>[] querySets + ) + { + var query = BuildQuery(querySets.SelectMany(static set => set)); + return string.IsNullOrWhiteSpace(query) ? path : $"{path}?{query}"; + } + + public static string BuildQuery(IEnumerable> values) + { + var segments = values + .Where(static pair => !string.IsNullOrWhiteSpace(pair.Value)) + .Select(static pair => + $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value!)}" + ) + .ToArray(); + + return string.Join("&", segments); + } + + public static string Escape(string value) => Uri.EscapeDataString(value); +} diff --git a/dotnet/src/CareEvolution.Orchestrate/StandardizeRequest.cs b/dotnet/src/CareEvolution.Orchestrate/StandardizeRequest.cs new file mode 100644 index 0000000..4557de5 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/StandardizeRequest.cs @@ -0,0 +1,12 @@ +namespace CareEvolution.Orchestrate; + +public sealed class StandardizeRequest +{ + public string? Code { get; set; } + + public string? System { get; set; } + + public string? Display { get; set; } + + public string? TargetSystem { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/StandardizeResponse.cs b/dotnet/src/CareEvolution.Orchestrate/StandardizeResponse.cs new file mode 100644 index 0000000..3fb481a --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/StandardizeResponse.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class StandardizeResponse +{ + public List Coding { get; set; } = []; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/StandardizeResponseCoding.cs b/dotnet/src/CareEvolution.Orchestrate/StandardizeResponseCoding.cs new file mode 100644 index 0000000..e318bfa --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/StandardizeResponseCoding.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate; + +public sealed class StandardizeResponseCoding +{ + public string? System { get; set; } + + public string? Code { get; set; } + + public string? Display { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4CodeSystemRequest.cs b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4CodeSystemRequest.cs new file mode 100644 index 0000000..37c6d16 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4CodeSystemRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class SummarizeFhirR4CodeSystemRequest +{ + public required string CodeSystem { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetRequest.cs b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetRequest.cs new file mode 100644 index 0000000..6f8d5ec --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class SummarizeFhirR4ValueSetRequest +{ + public required string Id { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetScopeRequest.cs b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetScopeRequest.cs new file mode 100644 index 0000000..fb1014f --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/SummarizeFhirR4ValueSetScopeRequest.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate; + +public sealed class SummarizeFhirR4ValueSetScopeRequest +{ + public required string Scope { get; set; } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs b/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs new file mode 100644 index 0000000..0c1888c --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs @@ -0,0 +1,451 @@ +namespace CareEvolution.Orchestrate; + +public interface ITerminologyApi +{ + Task ClassifyConditionAsync( + ClassifyConditionRequest request, + CancellationToken cancellationToken = default + ); + Task> ClassifyConditionAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task ClassifyMedicationAsync( + ClassifyMedicationRequest request, + CancellationToken cancellationToken = default + ); + Task> ClassifyMedicationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task ClassifyObservationAsync( + ClassifyObservationRequest request, + CancellationToken cancellationToken = default + ); + Task> ClassifyObservationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task GetAllFhirR4ValueSetsForCodesAsync( + Parameters request, + CancellationToken cancellationToken = default + ); + Task GetFhirR4CodeSystemAsync( + GetFhirR4CodeSystemRequest request, + CancellationToken cancellationToken = default + ); + Task GetFhirR4ConceptMapsAsync(CancellationToken cancellationToken = default); + Task GetFhirR4ValueSetAsync( + GetFhirR4ValueSetRequest request, + CancellationToken cancellationToken = default + ); + Task GetFhirR4ValueSetsByScopeAsync( + GetFhirR4ValueSetsByScopeRequest request, + CancellationToken cancellationToken = default + ); + Task GetFhirR4ValueSetScopesAsync(CancellationToken cancellationToken = default); + Task StandardizeBundleAsync( + Bundle bundle, + CancellationToken cancellationToken = default + ); + Task StandardizeConditionAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeConditionAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task StandardizeLabAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeLabAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task StandardizeMedicationAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeMedicationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task StandardizeObservationAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeObservationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task StandardizeProcedureAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeProcedureAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task StandardizeRadiologyAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ); + Task> StandardizeRadiologyAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ); + Task SummarizeFhirR4CodeSystemAsync( + SummarizeFhirR4CodeSystemRequest request, + CancellationToken cancellationToken = default + ); + Task SummarizeFhirR4CodeSystemsAsync(CancellationToken cancellationToken = default); + Task SummarizeFhirR4ValueSetAsync( + SummarizeFhirR4ValueSetRequest request, + CancellationToken cancellationToken = default + ); + Task SummarizeFhirR4ValueSetScopeAsync( + SummarizeFhirR4ValueSetScopeRequest request, + CancellationToken cancellationToken = default + ); + Task TranslateFhirR4ConceptMapAsync( + TranslateFhirR4ConceptMapRequest request, + CancellationToken cancellationToken = default + ); +} + +public sealed class TerminologyApi : ITerminologyApi +{ + private readonly OrchestrateHttpClient _http; + + internal TerminologyApi(OrchestrateHttpClient http) + { + _http = http; + } + + public Task ClassifyConditionAsync( + ClassifyConditionRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/classify/condition", + request, + cancellationToken + ); + + public Task> ClassifyConditionAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/classify/condition", + request, + cancellationToken + ); + + public Task ClassifyMedicationAsync( + ClassifyMedicationRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/classify/medication", + request, + cancellationToken + ); + + public Task> ClassifyMedicationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/classify/medication", + request, + cancellationToken + ); + + public Task ClassifyObservationAsync( + ClassifyObservationRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/classify/observation", + request, + cancellationToken + ); + + public Task> ClassifyObservationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/classify/observation", + request, + cancellationToken + ); + + public Task StandardizeConditionAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/condition", + request, + cancellationToken + ); + + public Task> StandardizeConditionAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/condition", + request, + cancellationToken + ); + + public Task StandardizeMedicationAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/medication", + request, + cancellationToken + ); + + public Task> StandardizeMedicationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/medication", + request, + cancellationToken + ); + + public Task StandardizeObservationAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/observation", + request, + cancellationToken + ); + + public Task> StandardizeObservationAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/observation", + request, + cancellationToken + ); + + public Task StandardizeProcedureAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/procedure", + request, + cancellationToken + ); + + public Task> StandardizeProcedureAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/procedure", + request, + cancellationToken + ); + + public Task StandardizeLabAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/lab", + request, + cancellationToken + ); + + public Task> StandardizeLabAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/lab", + request, + cancellationToken + ); + + public Task StandardizeRadiologyAsync( + StandardizeRequest request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/radiology", + request, + cancellationToken + ); + + public Task> StandardizeRadiologyAsync( + IReadOnlyList request, + CancellationToken cancellationToken = default + ) => + PostBatchAsync( + "/terminology/v1/standardize/radiology", + request, + cancellationToken + ); + + public Task StandardizeBundleAsync( + Bundle bundle, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/standardize/fhir/r4", + bundle, + cancellationToken + ); + + public Task GetFhirR4CodeSystemAsync( + GetFhirR4CodeSystemRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + $"/terminology/v1/fhir/r4/codesystem/{request.CodeSystem}", + [ + new KeyValuePair("page.num", request.PageNumber?.ToString()), + new KeyValuePair("_count", request.PageSize?.ToString()), + new KeyValuePair("concept:contains", request.ConceptContains), + ] + ); + return _http.GetJsonAsync(route, cancellationToken); + } + + public Task SummarizeFhirR4CodeSystemsAsync( + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + "/terminology/v1/fhir/r4/codesystem?_summary=true", + cancellationToken + ); + + public Task GetFhirR4ConceptMapsAsync(CancellationToken cancellationToken = default) => + _http.GetJsonAsync("/terminology/v1/fhir/r4/conceptmap", cancellationToken); + + public Task TranslateFhirR4ConceptMapAsync( + TranslateFhirR4ConceptMapRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/terminology/v1/fhir/r4/conceptmap/$translate", + [ + new KeyValuePair("code", request.Code), + new KeyValuePair("domain", request.Domain), + ] + ); + return _http.GetJsonAsync(route, cancellationToken); + } + + public Task SummarizeFhirR4ValueSetScopeAsync( + SummarizeFhirR4ValueSetScopeRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/terminology/v1/fhir/r4/valueset", + [ + new KeyValuePair("extension.scope", request.Scope), + new KeyValuePair("_summary", "true"), + ] + ); + return _http.GetJsonAsync(route, cancellationToken); + } + + public Task SummarizeFhirR4ValueSetAsync( + SummarizeFhirR4ValueSetRequest request, + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + $"/terminology/v1/fhir/r4/valueset/{RouteBuilder.Escape(request.Id)}?_summary=true", + cancellationToken + ); + + public Task GetFhirR4ValueSetAsync( + GetFhirR4ValueSetRequest request, + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + $"/terminology/v1/fhir/r4/valueset/{RouteBuilder.Escape(request.Id)}", + cancellationToken + ); + + public Task GetFhirR4ValueSetsByScopeAsync( + GetFhirR4ValueSetsByScopeRequest request, + CancellationToken cancellationToken = default + ) + { + var route = RouteBuilder.Build( + "/terminology/v1/fhir/r4/valueset", + [ + new KeyValuePair("name", request.Name), + new KeyValuePair("page.num", request.PageNumber?.ToString()), + new KeyValuePair("_count", request.PageSize?.ToString()), + new KeyValuePair("extension.scope", request.Scope), + ] + ); + return _http.GetJsonAsync(route, cancellationToken); + } + + public Task GetFhirR4ValueSetScopesAsync( + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + "/terminology/v1/fhir/r4/valueset/Rosetta.ValueSetScopes", + cancellationToken + ); + + public Task GetAllFhirR4ValueSetsForCodesAsync( + Parameters request, + CancellationToken cancellationToken = default + ) => + _http.PostJsonAsync( + "/terminology/v1/fhir/r4/valueset/$classify", + request, + cancellationToken + ); + + public Task SummarizeFhirR4CodeSystemAsync( + SummarizeFhirR4CodeSystemRequest request, + CancellationToken cancellationToken = default + ) => + _http.GetJsonAsync( + $"/terminology/v1/fhir/r4/codesystem/{RouteBuilder.Escape(request.CodeSystem)}?_summary=true", + cancellationToken + ); + + private async Task> PostBatchAsync( + string route, + IReadOnlyList request, + CancellationToken cancellationToken + ) + { + var response = await _http + .PostJsonAsync>( + $"{route}/batch", + new BatchRequest { Items = request }, + cancellationToken + ) + .ConfigureAwait(false); + return response.Items; + } +} diff --git a/dotnet/src/CareEvolution.Orchestrate/TranslateFhirR4ConceptMapRequest.cs b/dotnet/src/CareEvolution.Orchestrate/TranslateFhirR4ConceptMapRequest.cs new file mode 100644 index 0000000..a598d50 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/TranslateFhirR4ConceptMapRequest.cs @@ -0,0 +1,8 @@ +namespace CareEvolution.Orchestrate; + +public sealed class TranslateFhirR4ConceptMapRequest +{ + public required string Code { get; set; } + + public string? Domain { get; set; } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs new file mode 100644 index 0000000..d97f087 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -0,0 +1,344 @@ +using System.Text.Json; +using CareEvolution.Orchestrate.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class ApiSurfaceTests +{ + [Fact] + public async Task TerminologyBatchShouldPostToBatchRoute() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"items":[{"coding":[]}]}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + var response = await api.Terminology.StandardizeConditionAsync([ + new StandardizeRequest { Code = "123", System = "SNOMED" }, + ]); + + Assert.Single(response); + Assert.NotNull(handler.LastRequest); + Assert.Equal( + "https://api.example.com/terminology/v1/standardize/condition/batch", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + Assert.Contains("\"items\"", handler.LastRequest.Body); + } + + [Fact] + public async Task ConvertHl7ShouldSendPlainTextAndQueryParameters() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("{}")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + await api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request + { + Content = "MSH|^~\\&|", + PatientId = "patient-1", + PatientIdentifier = "identifier-1", + PatientIdentifierSystem = "urn:test", + Tz = "America/New_York", + ProcessingHint = "lab", + } + ); + + Assert.NotNull(handler.LastRequest); + Assert.Equal( + "https://api.example.com/convert/v1/hl7tofhirr4?patientId=patient-1&patientIdentifier=identifier-1&patientIdentifierSystem=urn%3Atest&tz=America%2FNew_York&processingHint=lab", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + Assert.Equal("text/plain", handler.LastRequest.Headers["Content-Type"].Single()); + Assert.Equal("MSH|^~\\&|", handler.LastRequest.Body); + } + + [Fact] + public async Task AdvancedTransportShouldApplyBaseUrlAndDeserializeJson() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + var response = await api.HttpHandler.GetJsonAsync( + "/custom/v1/ping" + ); + + Assert.Equal("ok", response.Message); + Assert.Equal( + "https://api.example.com/custom/v1/ping", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + Assert.Same(httpClient, api.HttpHandler.HttpClient); + } + + [Fact] + public async Task AdvancedTransportShouldSupportTextResponses() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Text("", "text/html")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + using var content = new StringContent(""); + var response = await api.HttpHandler.SendAsync( + HttpMethod.Post, + "/custom/v1/render", + content, + "text/html" + ); + + Assert.Equal("", response); + Assert.Equal("text/html", handler.LastRequest!.Headers["Accept"].Single()); + } + + [Fact] + public void AddOrchestrateApiShouldRegisterIOrchestrateApi() + { + var services = new ServiceCollection(); + + services.AddOrchestrateApi(options => + { + options.BaseUrl = "https://api.example.com"; + options.ApiKey = "test-api-key"; + options.TimeoutMs = 1234; + }); + + using var serviceProvider = services.BuildServiceProvider(); + using var api = serviceProvider.GetRequiredService(); + var options = serviceProvider + .GetRequiredService>() + .Value; + + Assert.NotNull(api); + Assert.IsType(api); + Assert.NotNull(api.Terminology); + Assert.NotNull(api.Convert); + Assert.NotNull(api.Insight); + Assert.Equal("https://api.example.com", options.BaseUrl); + Assert.Equal("test-api-key", options.ApiKey); + Assert.Equal(1234, options.TimeoutMs); + } + + [Fact] + public async Task ConvertPdfShouldReturnBytes() + { + var expected = new byte[] { 1, 2, 3, 4 }; + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Bytes(expected, "application/pdf")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + var response = await api.Convert.CdaToPdfAsync( + new ConvertCdaToPdfRequest { Content = "" } + ); + + Assert.Equal(expected, response); + Assert.Equal("application/pdf", handler.LastRequest!.Headers["Accept"].Single()); + } + + [Fact] + public async Task InsightRiskProfileShouldBuildExpectedQueryString() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("{}")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + await api.Insight.RiskProfileAsync( + new InsightRiskProfileRequest + { + Content = new Bundle(), + HccVersion = "24", + PeriodEndDate = "2026-01-01", + RaSegment = "community nondual aged", + } + ); + + Assert.Equal( + "https://api.example.com/insight/v1/riskprofile?hcc_version=24&period_end_date=2026-01-01&ra_segment=community%20nondual%20aged", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + Assert.Equal( + "application/json; charset=utf-8", + handler.LastRequest.Headers["Content-Type"].Single() + ); + } + + [Fact] + public void CombinedFhirBundleFactoryShouldGenerateNdjson() + { + var request = ConvertRequestFactory.GenerateConvertCombinedFhirBundlesRequestFromBundles( + [new Bundle(), new Bundle()], + "person-1" + ); + + Assert.Equal("person-1", request.PatientId); + Assert.Equal(1, request.Content.Count(character => character == '\n')); + Assert.DoesNotContain("\"ResourceType\":", request.Content, StringComparison.Ordinal); + Assert.Contains("\"resourceType\":\"Bundle\"", request.Content); + } + + [Fact] + public async Task StandardizeBundleShouldSerializeAsFhirJson() + { + Assert.Equal(Hl7.Fhir.Model.BundleType.BatchResponse, LiveTestData.R4Bundle.Type); + Assert.NotEmpty(LiveTestData.R4Bundle.Entry); + + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"resourceType":"Bundle","type":"collection","entry":[]}""" + ) + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + await api.Terminology.StandardizeBundleAsync(LiveTestData.R4Bundle); + + Assert.NotNull(handler.LastRequest); + Assert.Contains("\"resourceType\":\"Bundle\"", handler.LastRequest!.Body); + Assert.DoesNotContain( + "\"ResourceType\":", + handler.LastRequest.Body, + StringComparison.Ordinal + ); + Assert.Contains("\"type\":\"batch-response\"", handler.LastRequest.Body); + } + + [Fact] + public async Task GetAllFhirR4ValueSetsForCodesShouldSerializeParametersWithValueString() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json("""{"resourceType":"Parameters","parameter":[]}""") + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + var parameters = new Parameters + { + Parameter = + [ + new Parameters.ParameterComponent + { + Name = "code", + Value = new Hl7.Fhir.Model.FhirString("119981000146107"), + }, + new Parameters.ParameterComponent + { + Name = "system", + Value = new Hl7.Fhir.Model.FhirString("http://snomed.info/sct"), + }, + ], + }; + + await api.Terminology.GetAllFhirR4ValueSetsForCodesAsync(parameters); + + Assert.NotNull(handler.LastRequest); + Assert.Contains("\"resourceType\":\"Parameters\"", handler.LastRequest!.Body); + Assert.Contains("\"name\":\"code\"", handler.LastRequest.Body); + Assert.Contains("\"valueString\":\"119981000146107\"", handler.LastRequest.Body); + Assert.DoesNotContain( + "\"ResourceType\":", + handler.LastRequest.Body, + StringComparison.Ordinal + ); + } + + [Fact] + public async Task ConvertFhirR4ToOmopShouldSerializeAsFhirJson() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Bytes([80, 75, 3, 4], "application/zip")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + _ = await api.Convert.FhirR4ToOmopAsync( + new ConvertFhirR4ToOmopRequest { Content = LiveTestData.R4Bundle } + ); + + Assert.NotNull(handler.LastRequest); + Assert.Contains("\"resourceType\":\"Bundle\"", handler.LastRequest!.Body); + Assert.DoesNotContain( + "\"ResourceType\":", + handler.LastRequest.Body, + StringComparison.Ordinal + ); + Assert.Equal("application/zip", handler.LastRequest.Headers["Accept"].Single()); + } + + [Fact] + public async Task ConvertFhirR4ToNemsisV35ShouldSerializeAsFhirJson() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Text("", "application/xml")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + _ = await api.Convert.FhirR4ToNemsisV35Async( + new ConvertFhirR4ToNemsisV35Request { Content = LiveTestData.NemsisBundle } + ); + + Assert.NotNull(handler.LastRequest); + Assert.Contains("\"resourceType\":\"Bundle\"", handler.LastRequest!.Body); + Assert.DoesNotContain( + "\"ResourceType\":", + handler.LastRequest.Body, + StringComparison.Ordinal + ); + Assert.Equal("application/xml", handler.LastRequest.Headers["Accept"].Single()); + } + + private sealed class TransportProbeResponse + { + public string? Message { get; set; } + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/AssemblyInfo.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..2171200 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/CareEvolution.Orchestrate.Tests.csproj b/dotnet/tests/CareEvolution.Orchestrate.Tests/CareEvolution.Orchestrate.Tests.csproj new file mode 100644 index 0000000..ee6474f --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/CareEvolution.Orchestrate.Tests.csproj @@ -0,0 +1,32 @@ + + + net8.0;net10.0 + enable + enable + false + latest + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + PreserveNewest + + + diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs new file mode 100644 index 0000000..d3d1272 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -0,0 +1,134 @@ +using System.Net; +using CareEvolution.Orchestrate.Exceptions; +using CareEvolution.Orchestrate.Tests.Helpers; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class ConfigurationTests +{ + [Fact] + public async Task OrchestrateApiShouldPreferConstructorValuesOverEnvironmentVariables() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_BASE_URL"] = "https://env.example.com", + ["ORCHESTRATE_API_KEY"] = "env-api-key", + ["ORCHESTRATE_TIMEOUT_MS"] = "45000", + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = + """{"x-custom-header":"custom-value","x-api-key":"wrong"}""", + } + ); + + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions + { + ApiKey = "constructor-api-key", + BaseUrl = "https://constructor.example.com", + TimeoutMs = 30000, + } + ); + + await api.Terminology.StandardizeConditionAsync( + new StandardizeRequest { Code = "123", System = "SNOMED" } + ); + + Assert.NotNull(handler.LastRequest); + Assert.Equal( + "https://constructor.example.com/terminology/v1/standardize/condition", + handler.LastRequest!.RequestUri!.ToString() + ); + Assert.Equal("constructor-api-key", handler.LastRequest.Headers["x-api-key"].Single()); + Assert.Equal("custom-value", handler.LastRequest.Headers["x-custom-header"].Single()); + Assert.Equal("application/json", handler.LastRequest.Headers["Accept"].Single()); + } + + [Fact] + public void OrchestrateApiShouldThrowForInvalidTimeoutEnvironmentVariable() + { + using var environment = new EnvironmentVariableScope( + new Dictionary { ["ORCHESTRATE_TIMEOUT_MS"] = "not-a-number" } + ); + + var exception = Assert.Throws(() => new OrchestrateApi()); + Assert.Contains("ORCHESTRATE_TIMEOUT_MS", exception.Message); + } + + [Fact] + public void IdentityApiShouldRequireUrl() + { + using var environment = new EnvironmentVariableScope( + new Dictionary { ["ORCHESTRATE_IDENTITY_URL"] = null } + ); + + var exception = Assert.Throws(() => new IdentityApi()); + Assert.Contains("Identity URL is required", exception.Message); + } + + [Theory] + [InlineData("metrics-key", "Basic metrics-key")] + [InlineData("Basic metrics-key", "Basic metrics-key")] + public async Task IdentityApiShouldNormalizeMetricsKey( + string rawMetricsKey, + string expectedAuthorization + ) + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_URL"] = "https://identity.example.com", + } + ); + + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"datasourceOverlapRecords":[]}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new IdentityApi( + httpClient, + new IdentityApiOptions { MetricsKey = rawMetricsKey } + ); + + await api.Monitoring.OverlapMetricsAsync(); + + Assert.NotNull(handler.LastRequest); + Assert.Equal(expectedAuthorization, handler.LastRequest!.Headers["Authorization"].Single()); + } + + [Fact] + public async Task HttpErrorsShouldBeConvertedToOrchestrateClientErrors() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"issue":[{"severity":"error","code":"invalid","diagnostics":"Expected a Bundle but found a Patient"}]}""", + HttpStatusCode.BadRequest + ) + ) + ); + + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + var exception = await Assert.ThrowsAsync(() => + api.Terminology.StandardizeConditionAsync( + new StandardizeRequest { Code = "123", System = "SNOMED" } + ) + ); + + Assert.Contains( + "error: invalid - Expected a Bundle but found a Patient", + exception.Message + ); + Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/GlobalUsings.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/GlobalUsings.cs new file mode 100644 index 0000000..a60cdd4 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using CareEvolution.Orchestrate; +global using CareEvolution.Orchestrate.Identity; +global using Xunit; +global using Bundle = Hl7.Fhir.Model.R4.Bundle; +global using Parameters = Hl7.Fhir.Model.Parameters; diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/EnvironmentVariableScope.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/EnvironmentVariableScope.cs new file mode 100644 index 0000000..450b311 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/EnvironmentVariableScope.cs @@ -0,0 +1,23 @@ +namespace CareEvolution.Orchestrate.Tests.Helpers; + +internal sealed class EnvironmentVariableScope : IDisposable +{ + private readonly Dictionary _originalValues = new(StringComparer.Ordinal); + + public EnvironmentVariableScope(IDictionary values) + { + foreach (var pair in values) + { + _originalValues[pair.Key] = Environment.GetEnvironmentVariable(pair.Key); + Environment.SetEnvironmentVariable(pair.Key, pair.Value); + } + } + + public void Dispose() + { + foreach (var pair in _originalValues) + { + Environment.SetEnvironmentVariable(pair.Key, pair.Value); + } + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/FakeHttpMessageHandler.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/FakeHttpMessageHandler.cs new file mode 100644 index 0000000..68ee95b --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/FakeHttpMessageHandler.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Text; + +namespace CareEvolution.Orchestrate.Tests.Helpers; + +internal sealed class FakeHttpMessageHandler : HttpMessageHandler +{ + private readonly Func< + HttpRequestMessage, + CancellationToken, + Task + > _responder; + + public FakeHttpMessageHandler( + Func> responder + ) + { + _responder = responder; + } + + public RequestSnapshot? LastRequest { get; private set; } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + LastRequest = await RequestSnapshot.CreateAsync(request, cancellationToken); + return await _responder(request, cancellationToken); + } +} + +internal sealed record RequestSnapshot( + HttpMethod Method, + Uri? RequestUri, + IReadOnlyDictionary> Headers, + string? Body +) +{ + public static async Task CreateAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + var contentHeaders = request.Content is null + ? Enumerable.Empty>>() + : request.Content.Headers; + + var headers = request + .Headers.Concat(contentHeaders) + .ToDictionary( + pair => pair.Key, + pair => (IReadOnlyList)pair.Value.ToList(), + StringComparer.OrdinalIgnoreCase + ); + + var body = request.Content is null + ? null + : await request.Content.ReadAsStringAsync(cancellationToken); + + return new RequestSnapshot(request.Method, request.RequestUri, headers, body); + } +} + +internal static class FakeResponses +{ + public static HttpResponseMessage Json( + string json, + HttpStatusCode statusCode = HttpStatusCode.OK + ) + { + return new HttpResponseMessage(statusCode) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }; + } + + public static HttpResponseMessage Text( + string text, + string mediaType, + HttpStatusCode statusCode = HttpStatusCode.OK + ) + { + return new HttpResponseMessage(statusCode) + { + Content = new StringContent(text, Encoding.UTF8, mediaType), + }; + } + + public static HttpResponseMessage Bytes( + byte[] bytes, + string mediaType, + HttpStatusCode statusCode = HttpStatusCode.OK + ) + { + return new HttpResponseMessage(statusCode) + { + Content = new ByteArrayContent(bytes) + { + Headers = + { + ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mediaType), + }, + }, + }; + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs new file mode 100644 index 0000000..ae24fb5 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs @@ -0,0 +1,10 @@ +namespace CareEvolution.Orchestrate.Tests.Helpers; + +internal static class LiveClients +{ + public static OrchestrateApi CreateOrchestrateApi() => new(); + + public static IdentityApi CreateIdentityApi() => new(); + + public static LocalHashingApi CreateLocalHashingApi() => new(); +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestAttributes.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestAttributes.cs new file mode 100644 index 0000000..38df5dd --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestAttributes.cs @@ -0,0 +1,170 @@ +using System.Net.Sockets; + +namespace CareEvolution.Orchestrate.Tests.Helpers; + +internal static class LiveTestEnvironment +{ + private static readonly object SyncRoot = new(); + private static bool _loaded; + + public const string OrchestrateApiKey = "ORCHESTRATE_API_KEY"; + public const string IdentityApiKey = "ORCHESTRATE_IDENTITY_API_KEY"; + public const string IdentityMetricsKey = "ORCHESTRATE_IDENTITY_METRICS_KEY"; + public const string IdentityUrl = "ORCHESTRATE_IDENTITY_URL"; + public const string LocalHashingUrl = "ORCHESTRATE_IDENTITY_LOCAL_HASHING_URL"; + + public static string? GetSkipReason(params string[] requiredVariables) + { + EnsureEnvironmentLoaded(); + + var missingVariables = requiredVariables + .Where(variable => + string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(variable)) + ) + .ToArray(); + + if (missingVariables.Length == 0) + { + return GetUnavailableServiceReason(requiredVariables); + } + + return $"Live test requires environment variables: {string.Join(", ", missingVariables)}"; + } + + private static string? GetUnavailableServiceReason(IEnumerable requiredVariables) + { + foreach (var variable in requiredVariables) + { + if (variable is not (IdentityUrl or LocalHashingUrl)) + { + continue; + } + + var rawUrl = Environment.GetEnvironmentVariable(variable); + if ( + string.IsNullOrWhiteSpace(rawUrl) + || !Uri.TryCreate(rawUrl, UriKind.Absolute, out var uri) + ) + { + continue; + } + + if (!IsLocalHost(uri.Host)) + { + continue; + } + + var port = uri.IsDefaultPort + ? uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) + ? 443 + : 80 + : uri.Port; + + if (!CanConnect(uri.Host, port)) + { + return $"Live test requires {variable} service at {uri.Host}:{port} to be running."; + } + } + + return null; + } + + private static bool IsLocalHost(string host) => + host.Equals("localhost", StringComparison.OrdinalIgnoreCase) + || host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) + || host.Equals("::1", StringComparison.OrdinalIgnoreCase); + + private static bool CanConnect(string host, int port) + { + try + { + using var client = new TcpClient(); + var connectTask = client.ConnectAsync(host, port); + return connectTask.Wait(TimeSpan.FromMilliseconds(250)) && client.Connected; + } + catch + { + return false; + } + } + + private static void EnsureEnvironmentLoaded() + { + if (_loaded) + { + return; + } + + lock (SyncRoot) + { + if (_loaded) + { + return; + } + + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + var envPath = Path.Combine(current.FullName, ".env"); + if (File.Exists(envPath)) + { + LoadDotEnv(envPath); + break; + } + + current = current.Parent; + } + + _loaded = true; + } + } + + private static void LoadDotEnv(string path) + { + foreach (var rawLine in File.ReadAllLines(path)) + { + var line = rawLine.Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#')) + { + continue; + } + + var separatorIndex = line.IndexOf('='); + if (separatorIndex <= 0) + { + continue; + } + + var key = line[..separatorIndex].Trim(); + var value = line[(separatorIndex + 1)..].Trim(); + if ( + value.Length >= 2 + && ( + (value.StartsWith('"') && value.EndsWith('"')) + || (value.StartsWith('\'') && value.EndsWith('\'')) + ) + ) + { + value = value[1..^1]; + } + + Environment.SetEnvironmentVariable(key, value); + } + } +} + +public sealed class LiveFactAttribute : FactAttribute +{ + public LiveFactAttribute(params string[] requiredVariables) + { + Skip = LiveTestEnvironment.GetSkipReason(requiredVariables); + } +} + +public sealed class LiveTheoryAttribute : TheoryAttribute +{ + public LiveTheoryAttribute(params string[] requiredVariables) + { + Skip = LiveTestEnvironment.GetSkipReason(requiredVariables); + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestData.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestData.cs new file mode 100644 index 0000000..b16415e --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveTestData.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Hl7.Fhir.Serialization; + +namespace CareEvolution.Orchestrate.Tests.Helpers; + +internal static class LiveTestData +{ + private static readonly string BasePath = Path.Combine(AppContext.BaseDirectory, "LiveData"); + private static readonly FhirJsonFastParser FhirParser = new( + new ParserSettings(global::Hl7.Fhir.Model.Version.R4) + { + AllowUnrecognizedEnums = true, + AcceptUnknownMembers = true, + PermissiveParsing = true, + } + ); + + public static string Cda => ReadText("cda.xml"); + + public static string EncodingCda => ReadText("encoding_cda.xml"); + + public static string Hl7 => ReadText("hl7.txt"); + + public static Bundle R4Bundle => ReadBundle("r4_bundle.json"); + + public static Bundle RiskProfileBundle => ReadBundle("risk_profile_bundle.json"); + + public static string X12Document => ReadText("x12.txt"); + + public static JsonNode Dstu2Bundle => ReadJsonNode("dstu2_bundle.json"); + + public static JsonNode Stu3Bundle => ReadJsonNode("stu3_bundle.json"); + + public static Bundle NemsisBundle => ReadBundle("nemsis_bundle.json"); + + public static string BlindedDemographicData => + "H4sIAAAAAAAEA5yX2ZKj2BGG36WuO4Z9m4i5EItAbALEbs8FYkfsq6Cj392apiasmtK4" + + "bHODdCL4+PPkfzKT729z3A95U7/9Cn17S/K4jIa3X//x/W1c2/jt1zf2TL99e8uCIXv8oTZPz" + + "2FVw3H0gEU8aEbMNdFaLIRqQWYMiLn4bL9lt4Ajf3s81cZ9NY3B+KDvzHfMMt217WwgXfHPCQ" + + "Rh+jKzx3b/WQox5ETdglwmMNmX+v3Gcaifm2UeN3+gh7zKy6DPx/Wh+8e3738qVI+0T/t1kEU" + + "ASRKyP3mFgvG6Wjr98c5TZ6OnzmqlQZ54+isG/AXHnkhBrGAGLRWwlGq7Aj2NYHKuORA6n6ex" + + "5u18Ftm41HPhKjHhF7jtfHKq5XrhtS2+39cM2JnWZaLgZAvMWEZp2ypuXigdsTG7fYETZkkev" + + "btqEdZdOC+qqGrxcF7VAsAWYtQ4mvfwCgIrPdS/IN2xXcmBLEM4WCV2EaCUTgQ4mh0ZPCOiPH" + + "SghqulSXiqZP9M7PPu/2XbYNqswjqugSkEQ0RXBs0w9xcYzGI2XTzMYqqDZ6tADCy6gl+oC97" + + "FxWBVCpShKxMVQsG0Xq4hyfYsAhjjpazOHhsUZMr1X2XhrOMAxbg4PQF46B5aTSTjQ2Il6P4a" + + "qh2WVvN957IapuBSyhfqQvJQnG39UF2qJOpXAzO1Gt9RheooMAvOtCkAW38ISYNBvc84gnyyn" + + "Gw5Z0fcn1e55spn0HgAJ5Sf87xIMqWIOVeGBNCbYqRLuRce+YBznLrHV8UQFTd3vaoK2ku1w2" + + "ke3n+gHWiMNFAgfBkluXQfuC8kCuGxIjFQ0ANoJwTcflfKdoqN65RDfMDaSnvxbysHyPPdKl+" + + "ctA86YfUUEvhJInnGS6hZj+11C0f8fSPEPjNHqzkaNpzcVveQD19IHGvErI/1Avg8jyKpNcHX" + + "aTqtpaJ6+SE73vPrdDHcCaZ24YeXuN//SMt7CTzm/TCqQRX/uxA6ugpRQDkBCaY6knpd5WTTd" + + "17bGyclimWnNYhJqGBmXwbnn1b/u5I42tDmyCUMHW/WVseEdzu0OkQ1K7jhx9QuR/cEulKcAw" + + "D6Qu+Th+zBKoAOXd1HlZbVAs9jN99itbBA3YkgqBqbwOlhTd/EV/Z5As35LYAvs2wcvZ5nIrp" + + "as2DbcpKFvI4cDlgaUPgJv51sNv3PiphRsIwNcu4RJTTtrKVyiJySm2eVummYTi5xg47iFuoB" + + "L04v8aQI5jkbphI8ssb+Jqf7vjZu1SCQh9COtS9I59WEMwGyrc47QC+q3zMyFE+2oGGRjHoLa" + + "fmXAOzK2WtEDqH7MOuaijhhyVFcJuzVwXjS9n8b4rmUPjeyAGOHGjuBRMmlNk2pgghU7yFr4W" + + "naY81nOZl8p8ZmXMls84WVnztaPJgeuPjShTg4aaf1UXvtw3HQDytJS1pQsd1tCjBHlZlPLRb" + + "85Rnka+GWRCg2L4TY5ZELBlmFrOdcMI1pxA2VyWJoPHLuBHwB2qMA+E3lA5/01puixUvwCJ0o" + + "Mx3M/Mo93xOa7g1ggGqw/eSP5w1T+BDK71qVN5cI5y1jtoSlVeZGnUhaWKjOpFSj0REX0l6kE" + + "v95PTvDyNwalIy1M52IUeKI2qBEELWjfSqo5CotKVUoimn5afXC/p9xgY13JybRFpdgriJkoj" + + "4G+nv4dwTO+twouIghwUOWKPvyVXiRzs/glKUjRmulFTULLBJE3+Rrr70bxsW8Wpt0n9bV5bz" + + "bkVRfNDPiOasNbpHzEMG+HYmij7pM8d4kziZ0fq/2KL24knxg+PfJLSychXkxEnwAE4jLExxe" + + "Mo/xB6Ud0W3WWvNE656fbjdPrFGak4GjAs3SC7t8IC0kbOa45BwmxDof2w4tvMGuLySeTksZo" + + "5gEB2z1PjIsw/YiMx9wQUmEjLrBJamHoquuAiREV4rlp/dSotb2ORzT22PchS/W8Ml+j9HnqQ" + + "RUkdDHfQrgbFywTs3ZhZ91tFcrJ+ggbXLuDyt6OoMLfXqRCfyDMPc4eIqmeo9GS6UEoYCMMd6" + + "OQTYig0wpGQteJRLjYmCRXs1PH1A8EA1t0fotf09q4KYDQIGZxTGzhd1mJnEyGiLq4b6tt+5V" + + "c/1D2nM7lIO/dkNjJFkpSSGRmRx1MO784/hVFGBrjM6ALUnyeURbuFeYxs+j93c98H/E/F3lp" + + "EUTPSFpRJGk08SqzwRNz0Vu7fArCakWghmZyU2oV59ftFJkv9CnvALamlqKLzFlLm3WckXuc1" + + "EUjxzkZkAMaOMJ+TDzIIEAL5Lxgtc1smi148KRayjnUM6xWKVg1MS2U3qC03LPCzewt6W7wOy" + + "n6em56Jl1fWPrSdzQWIIiWTbU62QsXR6G1Hjo4MxyT5ozNUzI/1dF7za0t1gW/c3jVHVL4caB" + + "CuTGzvcVktr7lFIEuA4NV2X0i+Hhc22qjldJK3jVP7L0fQ+LdEq8DNt2MpWZhFiaNGCuUqA69" + + "K32U6SP8/Xzeq7L2NZrk0NcUFJieRCqICUmp2quu7u8bSWsOfYVYqN0wcNXuIeRf//2FkRzPj" + + "R9Hj++Sr+/5fUclHnExlWT9kGb5eHxz6/g33/8+BcAAAD//w=="; + + private static string ReadText(string fileName) => + File.ReadAllText(Path.Combine(BasePath, fileName)); + + private static Bundle ReadBundle(string fileName) => + (Bundle)FhirParser.Parse(ReadText(fileName), typeof(Bundle)); + + private static JsonNode ReadJsonNode(string fileName) => + JsonNode.Parse(ReadText(fileName)) + ?? throw new InvalidOperationException($"Unable to deserialize JSON fixture '{fileName}'."); +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs new file mode 100644 index 0000000..f8c41a0 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs @@ -0,0 +1,158 @@ +using CareEvolution.Orchestrate.Tests.Helpers; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class IdentityApiTests +{ + [Fact] + public async Task AddOrUpdateRecordShouldEncodeRouteAndSendDemographicPayload() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"matchedPerson":{"id":"1","records":[],"version":1,"status":{"code":"Active","supersededBy":[]}},"changedPersons":[],"advisories":{"invalidDemographicFields":[]}}""" + ) + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new IdentityApi( + httpClient, + new IdentityApiOptions { Url = "https://identity.example.com" } + ); + + await api.AddOrUpdateRecordAsync( + new AddOrUpdateRecordRequest + { + Source = "source one", + Identifier = "id+/=", + Demographic = new Demographic + { + FirstName = "John", + LastName = "Doe", + MedicaidId = "12345", + }, + } + ); + + Assert.Equal( + "https://identity.example.com/mpi/v1/record/source%20one/id%2B%2F%3D", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + Assert.Contains("\"firstName\":\"John\"", handler.LastRequest.Body); + Assert.Contains("\"medicaidID\":\"12345\"", handler.LastRequest.Body); + } + + [Fact] + public async Task AddOrUpdateBlindedRecordShouldFlattenPayload() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"matchedPerson":{"id":"1","records":[],"version":1,"status":{"code":"Active","supersededBy":[]}},"changedPersons":[]}""" + ) + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new IdentityApi( + httpClient, + new IdentityApiOptions { Url = "https://identity.example.com" } + ); + + await api.AddOrUpdateBlindedRecordAsync( + new AddOrUpdateBlindedRecordRequest + { + Source = "source", + Identifier = "identifier", + BlindedDemographic = new BlindedDemographic { Data = "abc", Version = 1 }, + } + ); + + Assert.DoesNotContain( + "blindedDemographic", + handler.LastRequest!.Body, + StringComparison.OrdinalIgnoreCase + ); + Assert.Contains("\"data\":\"abc\"", handler.LastRequest.Body); + } + + [Fact] + public async Task DeleteRecordShouldSendEmptyObjectPayload() + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"changedPersons":[]}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new IdentityApi( + httpClient, + new IdentityApiOptions { Url = "https://identity.example.com" } + ); + + await api.DeleteRecordAsync( + new CareEvolution.Orchestrate.Identity.Record + { + Source = "source", + Identifier = "identifier", + } + ); + + Assert.Equal("{}", handler.LastRequest!.Body); + } + + [Fact] + public async Task MonitoringShouldCallExpectedRoute() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"refreshed":"2026-01-01T00:00:00Z","totalRecordCount":1,"totalPersonCount":1,"globalMetricsRecords":[],"summaryMetricsRecords":[],"sourceTotals":[]}""" + ) + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new IdentityApi( + httpClient, + new IdentityApiOptions { Url = "https://identity.example.com" } + ); + + var response = await api.Monitoring.IdentifierMetricsAsync(); + + Assert.Equal(1, response.TotalRecordCount); + Assert.Equal( + "https://identity.example.com/monitoring/v1/identifierMetrics", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + } + + [Fact] + public async Task LocalHashingShouldUseConfiguredUrl() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_LOCAL_HASHING_URL"] = "https://hashing.example.com", + } + ); + + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json( + """{"data":"abc","version":1,"advisories":{"invalidDemographicFields":[]}}""" + ) + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new LocalHashingApi(httpClient); + + var response = await api.HashAsync(new Demographic { FirstName = "John" }); + + Assert.Equal("abc", response.Data); + Assert.Equal( + "https://hashing.example.com/hash", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs new file mode 100644 index 0000000..d1c3c3e --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs @@ -0,0 +1,1084 @@ +using System.IO.Compression; +using CareEvolution.Orchestrate.Exceptions; +using CareEvolution.Orchestrate.Tests.Helpers; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class LiveApiTests +{ + private static readonly byte[] PdfMagicNumber = [37, 80, 68, 70]; + private static readonly byte[] PkZipMagicNumber = [80, 75, 3, 4]; + private static readonly OrchestrateApi Api = LiveClients.CreateOrchestrateApi(); + private static readonly ClassifyConditionRequest[] ClassifyConditionRequestItems = + [ + new ClassifyConditionRequest + { + Code = "119981000146107", + System = "http://snomed.info/sct", + }, + new ClassifyConditionRequest { Code = "119981000146107", System = "SNOMED" }, + ]; + private static readonly ClassifyMedicationRequest[] ClassifyMedicationRequestItems = + [ + new ClassifyMedicationRequest + { + Code = "2468231", + System = "http://www.nlm.nih.gov/research/umls/rxnorm", + }, + new ClassifyMedicationRequest { Code = "2468231", System = "RxNorm" }, + ]; + private static readonly ClassifyObservationRequest[] ClassifyObservationRequestItems = + [ + new ClassifyObservationRequest { Code = "94558-4", System = "http://loinc.org" }, + new ClassifyObservationRequest { Code = "94558-4", System = "LOINC" }, + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeConditionCases = + [ + (new StandardizeRequest { Code = "370221004" }, "370221004"), + (new StandardizeRequest { Code = "J45.50" }, "J45.50"), + (new StandardizeRequest { Display = "dm2" }, "44054006"), + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeLabCases = + [ + (new StandardizeRequest { Code = "4548-4" }, "4548-4"), + (new StandardizeRequest { Display = "hba1c 1/15/22 from outside lab" }, "43396009"), + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeMedicationCases = + [ + (new StandardizeRequest { Code = "861004", System = "RxNorm" }, "861004"), + (new StandardizeRequest { Code = "59267-1000-02" }, "59267100002"), + ( + new StandardizeRequest + { + Display = "Jentadueto extended (linagliptin 2.5 / metFORMIN 1000mg)", + }, + "1796093" + ), + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeObservationCases = + [ + (new StandardizeRequest { Code = "8480-6" }, "8480-6"), + (new StandardizeRequest { Display = "BMI" }, "39156-5"), + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeProcedureCases = + [ + (new StandardizeRequest { Code = "80146002" }, "80146002"), + (new StandardizeRequest { Display = "ct head&neck" }, "429858000"), + ]; + private static readonly ( + StandardizeRequest Request, + string ExpectedCode + )[] StandardizeRadiologyCases = + [ + (new StandardizeRequest { Code = "711232001", System = "SNOMED" }, "711232001"), + ( + new StandardizeRequest { Display = "CT scan of head w/o iv contrast 3d ago@StJoes" }, + "30799-1" + ), + ]; + + public static TheoryData ClassifyConditionRequests => + new() { ClassifyConditionRequestItems[0], ClassifyConditionRequestItems[1] }; + + public static TheoryData ClassifyMedicationRequests => + new() { ClassifyMedicationRequestItems[0], ClassifyMedicationRequestItems[1] }; + + public static TheoryData ClassifyObservationRequests => + new() { ClassifyObservationRequestItems[0], ClassifyObservationRequestItems[1] }; + + public static TheoryData StandardizeConditionRequests => + new() + { + { StandardizeConditionCases[0].Request, StandardizeConditionCases[0].ExpectedCode }, + { StandardizeConditionCases[1].Request, StandardizeConditionCases[1].ExpectedCode }, + { StandardizeConditionCases[2].Request, StandardizeConditionCases[2].ExpectedCode }, + }; + + public static TheoryData StandardizeLabRequests => + new() + { + { StandardizeLabCases[0].Request, StandardizeLabCases[0].ExpectedCode }, + { StandardizeLabCases[1].Request, StandardizeLabCases[1].ExpectedCode }, + }; + + public static TheoryData StandardizeMedicationRequests => + new() + { + { StandardizeMedicationCases[0].Request, StandardizeMedicationCases[0].ExpectedCode }, + { StandardizeMedicationCases[1].Request, StandardizeMedicationCases[1].ExpectedCode }, + { StandardizeMedicationCases[2].Request, StandardizeMedicationCases[2].ExpectedCode }, + }; + + public static TheoryData StandardizeObservationRequests => + new() + { + { StandardizeObservationCases[0].Request, StandardizeObservationCases[0].ExpectedCode }, + { StandardizeObservationCases[1].Request, StandardizeObservationCases[1].ExpectedCode }, + }; + + public static TheoryData StandardizeProcedureRequests => + new() + { + { StandardizeProcedureCases[0].Request, StandardizeProcedureCases[0].ExpectedCode }, + { StandardizeProcedureCases[1].Request, StandardizeProcedureCases[1].ExpectedCode }, + }; + + public static TheoryData StandardizeRadiologyRequests => + new() + { + { StandardizeRadiologyCases[0].Request, StandardizeRadiologyCases[0].ExpectedCode }, + { StandardizeRadiologyCases[1].Request, StandardizeRadiologyCases[1].ExpectedCode }, + }; + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(ClassifyConditionRequests))] + public async Task ClassifyConditionShouldClassifySingleRequest(ClassifyConditionRequest request) + { + var result = await Api.Terminology.ClassifyConditionAsync(request); + Assert.NotNull(result); + Assert.True(result.CciAcute); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ClassifyConditionShouldClassifyBatch() + { + var results = await Api.Terminology.ClassifyConditionAsync(ClassifyConditionRequestItems); + Assert.Equal(2, results.Count); + Assert.All(results, result => Assert.True(result.CciAcute)); + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(ClassifyMedicationRequests))] + public async Task ClassifyMedicationShouldClassifySingleRequest( + ClassifyMedicationRequest request + ) + { + var result = await Api.Terminology.ClassifyMedicationAsync(request); + Assert.True(result.RxNormGeneric); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ClassifyMedicationShouldClassifyBatch() + { + var results = await Api.Terminology.ClassifyMedicationAsync(ClassifyMedicationRequestItems); + Assert.Equal(2, results.Count); + Assert.All(results, result => Assert.True(result.RxNormGeneric)); + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(ClassifyObservationRequests))] + public async Task ClassifyObservationShouldClassifySingleRequest( + ClassifyObservationRequest request + ) + { + var result = await Api.Terminology.ClassifyObservationAsync(request); + Assert.Equal("MICRO", result.LoincClass); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ClassifyObservationShouldClassifyBatch() + { + var results = await Api.Terminology.ClassifyObservationAsync( + ClassifyObservationRequestItems + ); + Assert.Equal(2, results.Count); + Assert.All(results, result => Assert.Equal("MICRO", result.LoincClass)); + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeConditionRequests))] + public async Task StandardizeConditionShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeConditionAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeConditionShouldStandardizeBatch() + { + var requests = StandardizeConditionCases.Select(row => row.Request).ToList(); + var expected = StandardizeConditionCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeConditionAsync(requests); + Assert.Equal(3, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeLabRequests))] + public async Task StandardizeLabShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeLabAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeLabShouldStandardizeBatch() + { + var requests = StandardizeLabCases.Select(row => row.Request).ToList(); + var expected = StandardizeLabCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeLabAsync(requests); + Assert.Equal(2, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeMedicationRequests))] + public async Task StandardizeMedicationShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeMedicationAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeMedicationShouldStandardizeBatch() + { + var requests = StandardizeMedicationCases.Select(row => row.Request).ToList(); + var expected = StandardizeMedicationCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeMedicationAsync(requests); + Assert.Equal(3, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeObservationRequests))] + public async Task StandardizeObservationShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeObservationAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeObservationShouldStandardizeBatch() + { + var requests = StandardizeObservationCases.Select(row => row.Request).ToList(); + var expected = StandardizeObservationCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeObservationAsync(requests); + Assert.Equal(2, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeProcedureRequests))] + public async Task StandardizeProcedureShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeProcedureAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeProcedureShouldStandardizeBatch() + { + var requests = StandardizeProcedureCases.Select(row => row.Request).ToList(); + var expected = StandardizeProcedureCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeProcedureAsync(requests); + Assert.Equal(2, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] + [MemberData(nameof(StandardizeRadiologyRequests))] + public async Task StandardizeRadiologyShouldStandardizeSingleRequest( + StandardizeRequest request, + string expectedCode + ) + { + var result = await Api.Terminology.StandardizeRadiologyAsync(request); + Assert.Contains(result.Coding, coding => coding.Code == expectedCode); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeRadiologyShouldStandardizeBatch() + { + var requests = StandardizeRadiologyCases.Select(row => row.Request).ToList(); + var expected = StandardizeRadiologyCases.Select(row => row.ExpectedCode).ToList(); + var results = await Api.Terminology.StandardizeRadiologyAsync(requests); + Assert.Equal(2, results.Count); + for (var index = 0; index < results.Count; index++) + { + Assert.Contains(results[index].Coding, coding => coding.Code == expected[index]); + } + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task StandardizeBundleShouldStandardize() + { + var result = await Api.Terminology.StandardizeBundleAsync(LiveTestData.R4Bundle); + Assert.NotNull(result.Entry); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithoutPatientShouldConvert() + { + var result = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } + ); + Assert.NotNull(result.Entry); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithPatientShouldConvert() + { + var result = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7, PatientId = "12/34" } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Equal("12/34", patient.Id); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithPatientIdentifierAndSystemShouldConvert() + { + var result = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request + { + Content = LiveTestData.Hl7, + PatientIdentifier = "1234", + PatientIdentifierSystem = "GoodHealthClinic", + } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.True( + patient.Identifier?.Count > 0 + && patient.Identifier[0].Use == "usual" + && patient + .Identifier[0] + .System?.Contains("GoodHealthClinic", StringComparison.Ordinal) == true + && patient.Identifier[0].Value == "1234" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithTimezoneShouldConvert() + { + var result = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7, Tz = "America/New_York" } + ); + var encounter = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Encounter + ); + Assert.Equal("2014-11-07T14:40:00-05:00", encounter.Period?.Start); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithoutTimezoneShouldPresumeUtc() + { + var result = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } + ); + var encounter = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Encounter + ); + Assert.Equal("2014-11-07T14:40:00+00:00", encounter.Period?.Start); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithLabProcessingHintShouldConvert() + { + const string content = """ + MSH|^~\&|||||20220309050000||ORU^R01|||2.7 + PID|1||123456||LastName^FirstName|||||||||||||5678 + PV1|1|I||||||||||||||||||||||||||||||||||||||||||20220309050000 + OBR||001CCK612||ABC^AUTOMATED BLOOD COUNT^LAB|||20220309134200|||2222^ORDERED,BY||||20220309134400|Specimen^|10341^Doctor MD^First^F||||W2622||||HE|F|CBC^ABC|^^^^^R|^~^~^||||||| + NTE|||Lab Report Comment + OBX|1|ST|^WBC^LAB|1|1.4|K/UL|4.5-11.5|L^LL|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|2|ST|^RBC^LAB|1|3.50|M/UL|4.3-5.9|L|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|3|ST|^HGB^LAB|1|11.6|GM/DL|13.9-16.3|L|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|4|ST|^HCT^LAB|1|33.7|%|39-55|L|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|5|ST|^MCV^LAB|1|96.4|FL|80-100||||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|6|ST|^MCH^LAB|1|33.1|PG|25.4-34.6||||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|7|ST|^MCHC^LAB|1|34.3|GM/DL|30-37||||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|8|ST|^RDW^LAB|1|17.9|%|11.5-14.5|H|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + OBX|9|ST|^PLATELETS^LAB|1|125|K/UL|130-400|L|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| + """; + + var hintedResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "lab" } + ); + var unhintedResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content } + ); + var defaultResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "default" } + ); + + Assert.Equal(9, CountResources(hintedResult, Hl7.Fhir.Model.ResourceType.Observation)); + Assert.Equal(0, CountResources(unhintedResult, Hl7.Fhir.Model.ResourceType.Observation)); + Assert.Equal(0, CountResources(defaultResult, Hl7.Fhir.Model.ResourceType.Observation)); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertHl7ToFhirR4WithTranscriptionProcessingHintShouldConvert() + { + const string content = """ + MSH|^~\&||TX|||20110706100000||ORU^R01|||2.3 + PID|1||123456||LastName^FirstName||20000101|M||||||||||7890 + PV1|1|I||||||||||||||||||||||||||||||||||||||||||20110706100000 + ORC|RE|^SCM|||||||20110706100000|||010400^DOE MD^JOHN^^^^ + OBR|1|^SCM|001XYZ555^SCM|CH9^CHEST SPECIAL VIEWS|||20110706100000|||||||20110706100000||010400^DOE MD^JOHN^^^^|||||||||P||^^^20110706100000^^R|~~~~||||010400^DOE MD^JOHN|~|^UNKNOWN^TECHNOLOGIST^^^^|010400^DOE MD^JOHN^^^^ + OBX|1|ST|&GDT^^GDT||Line 1||||||F + OBX|2|ST|&GDT^Label^GDT||Line 2||||||F + OBX|3|ST|&GDT^&Not a Label^GDT||Line 3||||||F + OBX|4|ST|Dictation TS|2|Dictated by: Tue Mar 18, 2025 1:06:45 PM EDT [INTERFACE, INCOMING RADIANT IMAGE AVAILABILITY]||||||Final|||||E175762^MILLER^AMANDA^^^^^^PROVID^^^^PROVID^^^^^^^^RT||||||||| + """; + + var hintedResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "transcription" } + ); + var unhintedResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content } + ); + var defaultResult = await Api.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "default" } + ); + + Assert.Equal(1, CountResources(hintedResult, Hl7.Fhir.Model.ResourceType.Binary)); + Assert.Equal(0, CountResources(unhintedResult, Hl7.Fhir.Model.ResourceType.Binary)); + Assert.Equal(0, CountResources(defaultResult, Hl7.Fhir.Model.ResourceType.Binary)); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToFhirR4WithoutPatientShouldConvert() + { + var result = await Api.Convert.CdaToFhirR4Async( + new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToFhirR4WithIncludeOriginalCdaShouldConvert() + { + var result = await Api.Convert.CdaToFhirR4Async( + new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, IncludeOriginalCda = true } + ); + Assert.Contains( + result.Entry, + entry => + entry.Resource?.ResourceType == Hl7.Fhir.Model.ResourceType.DocumentReference + && entry.Resource is Hl7.Fhir.Model.R4.DocumentReference doc + && doc.Type?.Coding?.Any(coding => coding.Code == "Cda") == true + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToFhirR4WithIncludeStandardizedCdaShouldConvert() + { + var result = await Api.Convert.CdaToFhirR4Async( + new ConvertCdaToFhirR4Request + { + Content = LiveTestData.Cda, + IncludeStandardizedCda = true, + } + ); + Assert.Contains( + result.Entry, + entry => + entry.Resource?.ResourceType == Hl7.Fhir.Model.ResourceType.DocumentReference + && entry.Resource is Hl7.Fhir.Model.R4.DocumentReference doc + && doc.Type?.Coding?.Any(coding => coding.Code == "StandardizedCda") == true + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToFhirR4WithPatientShouldConvert() + { + var result = await Api.Convert.CdaToFhirR4Async( + new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, PatientId = "1234" } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Equal("1234", patient.Id); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToFhirR4WithPatientIdentifierAndSystemShouldConvert() + { + var result = await Api.Convert.CdaToFhirR4Async( + new ConvertCdaToFhirR4Request + { + Content = LiveTestData.Cda, + PatientIdentifier = "1234", + PatientIdentifierSystem = "GoodHealthClinic", + } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.True( + patient.Identifier?.Count > 0 + && patient.Identifier[0].Use == "usual" + && patient + .Identifier[0] + .System?.Contains("GoodHealthClinic", StringComparison.Ordinal) == true + && patient.Identifier[0].Value == "1234" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToPdfShouldConvert() + { + var result = await Api.Convert.CdaToPdfAsync( + new ConvertCdaToPdfRequest { Content = LiveTestData.Cda } + ); + Assert.Equal(PdfMagicNumber, result.Take(4).ToArray()); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertFhirR4ToCdaShouldConvert() + { + var result = await Api.Convert.FhirR4ToCdaAsync( + new ConvertFhirR4ToCdaRequest { Content = LiveTestData.R4Bundle } + ); + Assert.StartsWith("( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Equal("1234", patient.Id); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCombinedFhirR4BundlesWithPatientIdentifierAndSystemShouldConvert() + { + var request = ConvertRequestFactory.GenerateConvertCombinedFhirBundlesRequestFromBundles([ + LiveTestData.R4Bundle, + LiveTestData.R4Bundle, + ]); + request.PatientIdentifier = "1234"; + request.PatientIdentifierSystem = "GoodHealthClinic"; + + var result = await Api.Convert.CombineFhirR4BundlesAsync(request); + Assert.Equal(2, result.Entry.Count); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.True( + patient.Identifier?.Count > 0 + && patient.Identifier[0].Use == "usual" + && patient + .Identifier[0] + .System?.Contains("GoodHealthClinic", StringComparison.Ordinal) == true + && patient.Identifier[0].Value == "1234" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertX12ToFhirR4ShouldReturnBundle() + { + var result = await Api.Convert.X12ToFhirR4Async( + new ConvertX12ToFhirR4Request { Content = LiveTestData.X12Document } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertX12ToFhirR4WithPatientShouldReturnBundle() + { + var result = await Api.Convert.X12ToFhirR4Async( + new ConvertX12ToFhirR4Request + { + Content = LiveTestData.X12Document, + PatientId = "12/34", + } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Equal("12/34", patient.Id); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertX12ToFhirR4WithPatientIdentifierAndSystemShouldConvert() + { + var result = await Api.Convert.X12ToFhirR4Async( + new ConvertX12ToFhirR4Request + { + Content = LiveTestData.X12Document, + PatientIdentifier = "1234", + PatientIdentifierSystem = "GoodHealthClinic", + } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.True( + patient.Identifier?.Count > 0 + && patient.Identifier[0].Use == "usual" + && patient + .Identifier[0] + .System?.Contains("GoodHealthClinic", StringComparison.Ordinal) == true + && patient.Identifier[0].Value == "1234" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4CodeSystemShouldReturnCodeSystem() + { + var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + new GetFhirR4CodeSystemRequest { CodeSystem = "SNOMED" } + ); + Assert.NotEmpty(result.Concept); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4CodeSystemWithPageShouldReturnCodeSystem() + { + var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + new GetFhirR4CodeSystemRequest + { + CodeSystem = "SNOMED", + PageNumber = 0, + PageSize = 2, + } + ); + Assert.NotEmpty(result.Concept); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4CodeSystemWithSearchShouldReturnCodeSystem() + { + var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + new GetFhirR4CodeSystemRequest + { + CodeSystem = "ICD-10-CM", + ConceptContains = "myocardial infarction", + PageNumber = 0, + PageSize = 2, + } + ); + Assert.NotEmpty(result.Concept); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task SummarizeFhirR4CodeSystemsShouldReturnBundle() + { + var result = await Api.Terminology.SummarizeFhirR4CodeSystemsAsync(); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ConceptMapsShouldReturnBundle() + { + var result = await Api.Terminology.GetFhirR4ConceptMapsAsync(); + Assert.NotEmpty(result.Entry); + Assert.All( + result.Entry, + entry => + Assert.Equal(Hl7.Fhir.Model.ResourceType.ConceptMap, entry.Resource?.ResourceType) + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task TranslateFhirR4ConceptMapWithCodeShouldTranslate() + { + var result = await Api.Terminology.TranslateFhirR4ConceptMapAsync( + new TranslateFhirR4ConceptMapRequest { Code = "119981000146107" } + ); + Assert.NotEmpty(result.Parameter); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task TranslateFhirR4ConceptMapWithCodeAndDomainShouldTranslate() + { + var result = await Api.Terminology.TranslateFhirR4ConceptMapAsync( + new TranslateFhirR4ConceptMapRequest { Code = "119981000146107", Domain = "Condition" } + ); + Assert.NotEmpty(result.Parameter); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task SummarizeFhirR4ValueSetScopeShouldReturnBundle() + { + var result = await Api.Terminology.SummarizeFhirR4ValueSetScopeAsync( + new SummarizeFhirR4ValueSetScopeRequest { Scope = "http://loinc.org" } + ); + Assert.NotEmpty(result.Entry); + Assert.True(result.Entry.Count <= 10000); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetShouldReturnValueSet() + { + var result = await Api.Terminology.GetFhirR4ValueSetAsync( + new GetFhirR4ValueSetRequest + { + Id = "00987FA2EDADBD0E43DA59E171B80F99DBF832C69904489EE6F9E6450925E5A2", + } + ); + Assert.NotNull(result.Compose?.Include); + Assert.NotEmpty(result.Compose.Include); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task SummarizeFhirR4ValueSetShouldReturnValueSet() + { + var result = await Api.Terminology.SummarizeFhirR4ValueSetAsync( + new SummarizeFhirR4ValueSetRequest + { + Id = "00987FA2EDADBD0E43DA59E171B80F99DBF832C69904489EE6F9E6450925E5A2", + } + ); + Assert.Equal(Hl7.Fhir.Model.ResourceType.ValueSet, result.ResourceType); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetScopesShouldReturnValueSet() + { + var result = await Api.Terminology.GetFhirR4ValueSetScopesAsync(); + Assert.NotNull(result.Compose?.Include); + Assert.NotEmpty(result.Compose.Include); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetsByScopeWithoutPaginationShouldRaise() + { + await Assert.ThrowsAsync(() => + Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + new GetFhirR4ValueSetsByScopeRequest { Scope = "http://loinc.org" } + ) + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetsByScopeWithPageAndScopeShouldReturnBundle() + { + var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + new GetFhirR4ValueSetsByScopeRequest + { + Scope = "http://loinc.org", + PageNumber = 0, + PageSize = 2, + } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetsByScopeWithPageAndNameShouldReturnBundle() + { + var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + new GetFhirR4ValueSetsByScopeRequest + { + Name = "LP7839-6", + PageNumber = 0, + PageSize = 2, + } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetsByScopeWithPageNameAndScopeShouldReturnBundle() + { + var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + new GetFhirR4ValueSetsByScopeRequest + { + Name = "LP7839-6", + Scope = "http://loinc.org", + PageNumber = 0, + PageSize = 2, + } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetFhirR4ValueSetsByScopeWithJustPageShouldReturnBundle() + { + var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + new GetFhirR4ValueSetsByScopeRequest { PageNumber = 0, PageSize = 2 } + ); + Assert.NotEmpty(result.Entry); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task SummarizeFhirR4CodeSystemShouldReturnCodeSystem() + { + var result = await Api.Terminology.SummarizeFhirR4CodeSystemAsync( + new SummarizeFhirR4CodeSystemRequest { CodeSystem = "SNOMED" } + ); + Assert.True(result.Count > 0); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task GetAllFhirR4ValueSetsForCodesShouldReturnParameters() + { + var parameters = new Parameters + { + Parameter = + [ + new Parameters.ParameterComponent + { + Name = "code", + Value = new Hl7.Fhir.Model.FhirString("119981000146107"), + }, + new Parameters.ParameterComponent + { + Name = "system", + Value = new Hl7.Fhir.Model.FhirString("http://snomed.info/sct"), + }, + ], + }; + + var result = await Api.Terminology.GetAllFhirR4ValueSetsForCodesAsync(parameters); + Assert.NotEmpty(result.Parameter); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertFhirDstu2ToFhirR4ShouldConvert() + { + var result = await Api.Convert.FhirDstu2ToFhirR4Async( + new ConvertFhirDstu2ToFhirR4Request { Content = LiveTestData.Dstu2Bundle } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Contains( + patient.Identifier, + identifier => identifier.ElementId == "id3" && identifier.Value == "12345A" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertFhirStu3ToFhirR4ShouldConvert() + { + var result = await Api.Convert.FhirStu3ToFhirR4Async( + new ConvertFhirStu3ToFhirR4Request { Content = LiveTestData.Stu3Bundle } + ); + var patient = GetEntryResourceByType( + result, + Hl7.Fhir.Model.ResourceType.Patient + ); + Assert.Contains( + patient.Identifier, + identifier => identifier.ElementId == "id3" && identifier.Value == "1234A" + ); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertFhirR4ToHealthLakeShouldConvert() + { + var result = await Api.Convert.FhirR4ToHealthLakeAsync( + new ConvertFhirR4ToHealthLakeRequest { Content = LiveTestData.R4Bundle } + ); + Assert.Equal(Hl7.Fhir.Model.BundleType.Collection, result.Type); + Assert.Equal(Hl7.Fhir.Model.ResourceType.Bundle, result.Entry[0].Resource?.ResourceType); + Assert.Equal(Hl7.Fhir.Model.BundleType.Batch, ((Bundle)result.Entry[0].Resource!).Type); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertCdaToHtmlShouldConvert() + { + var result = await Api.Convert.CdaToHtmlAsync( + new ConvertCdaToHtmlRequest { Content = LiveTestData.Cda } + ); + Assert.StartsWith(" entry.FullName).ToList(); + Assert.Contains("patients.csv", names); + Assert.Contains("encounters.csv", names); + Assert.Contains("procedures.csv", names); + Assert.Contains("conditions.csv", names); + Assert.All( + archive.Entries, + entry => + { + Assert.EndsWith(".csv", entry.FullName, StringComparison.Ordinal); + Assert.True(entry.Length > 0); + } + ); + + using var reader = new StreamReader(archive.GetEntry("patients.csv")!.Open()); + var content = await reader.ReadToEndAsync(); + Assert.Contains(",", content, StringComparison.Ordinal); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task ConvertFhirR4ToManifestWithDelimiterShouldHaveCsvsAndExpectedDelimiter() + { + var result = await Api.Convert.FhirR4ToManifestAsync( + new ConvertFhirR4ToManifestRequest { Content = LiveTestData.R4Bundle, Delimiter = "|" } + ); + + using var archive = new ZipArchive(new MemoryStream(result), ZipArchiveMode.Read); + using var reader = new StreamReader(archive.GetEntry("patients.csv")!.Open()); + var content = await reader.ReadToEndAsync(); + Assert.Contains("|", content, StringComparison.Ordinal); + } + + [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] + public async Task WithTimeoutShouldTimeout() + { + using var timeoutApi = new OrchestrateApi(new OrchestrateClientOptions { TimeoutMs = 1 }); + await Assert.ThrowsAnyAsync(() => + timeoutApi.Convert.Hl7ToFhirR4Async( + new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } + ) + ); + } + + private static int CountResources(Bundle bundle, Hl7.Fhir.Model.ResourceType resourceType) => + bundle.Entry.Count(entry => entry.Resource?.ResourceType == resourceType); + + private static TResource GetEntryResourceByType( + Bundle bundle, + Hl7.Fhir.Model.ResourceType resourceType + ) + where TResource : Hl7.Fhir.Model.Resource + { + return bundle.Entry.First(entry => entry.Resource?.ResourceType == resourceType).Resource + as TResource + ?? throw new InvalidOperationException($"Expected resource type '{resourceType}'."); + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/cda.xml b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/cda.xml new file mode 100644 index 0000000..027565b --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/cda.xml @@ -0,0 +1,70 @@ + + + + + + + + + + Medical Summary Document + + + + + + + + 34 Drury Lane + Disney Land + CA + 90210 + + + + + Patient + Smith + + + + + + + + + + + + +
+ + + + + Vital Signs + No Vital Signs Available + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/dstu2_bundle.json b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/dstu2_bundle.json new file mode 100644 index 0000000..9203f39 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/dstu2_bundle.json @@ -0,0 +1,2070 @@ +{ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "resource": { + "resourceType": "Patient", + "id": "3ead5e15-4da5-480b-8a94-ffea1e936809", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.723+00:00", + "profile": [ + "http://fhir.org/guides/argonaut/StructureDefinition/argo-patient" + ], + "security": [ + { + "system": "http://careevolution.com/accesspolicyname", + "code": "Standard Record Policy" + } + ] + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#patientInstitutions", + "valueCoding": { + "id": "patient-institution1", + "code": "BeaconInstitution", + "display": "BeaconInstitution" + } + } + ], + "identifier": [ + { + "id": "id1", + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest", + "value": "12345A" + }, + { + "id": "id2", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN", + "value": "12345A" + }, + { + "id": "id3", + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "name": [ + { + "id": "name1", + "use": "official", + "family": [ + "Smith" + ], + "given": [ + "John" + ] + } + ], + "gender": "male", + "_gender": { + "id": "gender1", + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "id": "gender2", + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/Gender", + "code": "M", + "display": "Male", + "userSelected": true + } + ] + } + } + ] + }, + "birthDate": "1961-01-14", + "_birthDate": { + "id": "birthdate1" + }, + "deceasedBoolean": false, + "_deceasedBoolean": { + "id": "deceased1" + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/0-3a4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "0-3a4295efae7fed11b9cc0e32e07a5c1b", + "contained": [ + { + "resourceType": "Patient", + "id": "patient", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "profile": [ + "http://fhir.org/guides/argonaut/StructureDefinition/argo-patient" + ], + "security": [ + { + "system": "http://careevolution.com/accesspolicyname", + "code": "Standard Record Policy" + } + ] + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#patientInstitutions", + "valueCoding": { + "id": "patient-institution1", + "code": "BeaconInstitution", + "display": "BeaconInstitution" + } + } + ], + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest", + "value": "12345A" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN", + "value": "12345A" + }, + { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "name": [ + { + "use": "official", + "_use": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/NameType", + "code": "LegalName", + "display": "Legal Name", + "userSelected": true + } + ] + } + } + ] + }, + "family": [ + "Smith" + ], + "given": [ + "John" + ] + } + ], + "gender": "male", + "_gender": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/Gender", + "code": "M", + "display": "Male", + "userSelected": true + } + ] + } + } + ] + }, + "birthDate": "1961-01-14", + "deceasedBoolean": false + } + ], + "target": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#name1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#birthdate1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#deceased1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#gender1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#gender2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#patient-institution1" + } + ], + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + } + ], + "period": { + "start": "2022-12-19T15:08:05.723+00:00", + "end": "2022-12-19T15:08:05.717+00:00" + }, + "recorded": "2022-12-19T15:08:05.717+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ], + "entity": [ + { + "role": "source", + "type": { + "system": "http://hl7.org/fhir/resource-types", + "code": "Patient" + }, + "reference": "#patient" + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Condition/5.d30a4c15f97fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.d30a4c15f97fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T23:58:55.387+00:00" + }, + "patient": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "flu", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "6142004", + "display": "Influenza (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "J11.1", + "display": "Influenza due to unidentified influenza virus with other respiratory manifestations", + "userSelected": false + } + ] + }, + "category": { + "coding": [ + { + "system": "http://argonaut.hl7.org", + "code": "health-concern" + }, + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "EncounterDiagnosis", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/fhir-diagnosis-role/Reference", + "code": "CC", + "display": "Chief complaint", + "userSelected": false + } + ] + }, + "clinicalStatus": "resolved", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "resolved", + "userSelected": true + } + ] + } + } + ] + }, + "_verificationStatus": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + }, + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "unconfirmed", + "userSelected": true + } + ] + } + } + ] + }, + "onsetDateTime": "2022-12-19T02:31:55.382-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Condition/5.3d4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.3d4295efae7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.75+00:00", + "profile": [ + "http://fhir.org/guides/argonaut/StructureDefinition/argo-condition" + ] + }, + "patient": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "multiple sclerosis", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "24700007", + "display": "Multiple sclerosis (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "G35", + "display": "Multiple sclerosis", + "userSelected": false + } + ] + }, + "category": { + "coding": [ + { + "system": "http://argonaut.hl7.org", + "code": "health-concern" + }, + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "EncounterDiagnosis", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/fhir-diagnosis-role/Reference", + "code": "CC", + "display": "Chief complaint", + "userSelected": false + } + ] + }, + "clinicalStatus": "resolved", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "inactive", + "userSelected": true + } + ] + } + } + ] + }, + "verificationStatus": "confirmed", + "_verificationStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "confirmed", + "userSelected": true + } + ] + } + } + ] + }, + "onsetDateTime": "2022-12-18T18:55:05.675-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Condition/5.3c4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.3c4295efae7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.74+00:00" + }, + "patient": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "crohns disease", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "34000006", + "display": "Crohn's disease (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "K50.90", + "display": "Crohn's disease, unspecified, without complications", + "userSelected": false + } + ] + }, + "category": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + } + ], + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "SECONDARY", + "display": "SECONDARY", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/DiagnosisType", + "code": "Secondary", + "display": "Secondary", + "userSelected": false + } + ] + }, + "clinicalStatus": "active", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "Active", + "userSelected": true + } + ] + } + } + ] + }, + "verificationStatus": "confirmed", + "_verificationStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "confirmed", + "userSelected": true + } + ] + } + } + ] + }, + "onsetDateTime": "2022-12-18T17:20:05.675-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/4-d30a4c15f97fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-d30a4c15f97fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/5.d30a4c15f97fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T23:58:55.387+00:00", + "end": "2022-12-19T23:58:55.387+00:00" + }, + "recorded": "2022-12-19T23:58:55.387+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/4-3d4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-3d4295efae7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/5.3d4295efae7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T15:08:05.75+00:00", + "end": "2022-12-19T15:08:05.75+00:00" + }, + "recorded": "2022-12-19T15:08:05.75+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/4-3c4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-3c4295efae7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/5.3c4295efae7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T15:08:05.74+00:00", + "end": "2022-12-19T15:08:05.74+00:00" + }, + "recorded": "2022-12-19T15:08:05.74+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.682d4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.682d4f56bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:44:05.61+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T13:45:05.605-05:00", + "issued": "2022-12-18T13:45:05.605-05:00", + "valueQuantity": { + "value": 1690728474 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.d12b4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.d12b4f56bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:44:03.04+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T14:56:03.025-05:00", + "issued": "2022-12-18T14:56:03.025-05:00", + "valueQuantity": { + "value": 1271126762 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.028d6f3ebc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.028d6f3ebc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:43:26.953+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T17:09:26.943-05:00", + "issued": "2022-12-18T17:09:26.943-05:00", + "valueQuantity": { + "value": 295302631 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.54e37a32bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.54e37a32bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:43:10.48+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T10:31:10.464-05:00", + "issued": "2022-12-19T10:31:10.464-05:00", + "valueQuantity": { + "value": 652309659 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.d24b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.d24b67f6bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:41:23.93+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T11:44:23.919-05:00", + "issued": "2022-12-18T11:44:23.919-05:00", + "valueQuantity": { + "value": 789662810 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.a94b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.a94b67f6bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:41:22.403+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T11:56:22.403-05:00", + "issued": "2022-12-18T11:56:22.403-05:00", + "valueQuantity": { + "value": 1582118206 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.f1b562d2bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.f1b562d2bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:40:27.56+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-18T22:38:27.543-05:00", + "issued": "2022-12-18T22:38:27.543-05:00", + "valueQuantity": { + "value": 252600252 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.2d6720babb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.2d6720babb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-20T03:22:56.517+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T11:21:45.582-05:00", + "issued": "2022-12-19T11:21:45.582-05:00", + "valueQuantity": { + "value": 1724700070 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.b185f6a7bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.b185f6a7bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:39:16.903+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T10:56:16.9-05:00", + "issued": "2022-12-19T10:56:16.9-05:00", + "valueQuantity": { + "value": 2126651898 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:37:09.9+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T07:24:09.891-05:00", + "issued": "2022-12-19T07:24:09.891-05:00", + "valueQuantity": { + "value": 941137949 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.1a5cf010bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.1a5cf010bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:35:02.83+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T10:44:02.819-05:00", + "issued": "2022-12-19T10:44:02.819-05:00", + "valueQuantity": { + "value": 1868889329 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Observation/1.f59dca04bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.f59dca04bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:34:40.32+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + }, + "effectiveDateTime": "2022-12-19T11:11:40.299-05:00", + "issued": "2022-12-19T11:11:40.299-05:00", + "valueQuantity": { + "value": 1577878080 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-682d4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-682d4f56bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.682d4f56bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:44:05.61+00:00", + "end": "2022-12-19T16:44:05.61+00:00" + }, + "recorded": "2022-12-19T16:44:05.61+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-d12b4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-d12b4f56bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.d12b4f56bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:44:03.04+00:00", + "end": "2022-12-19T16:44:03.04+00:00" + }, + "recorded": "2022-12-19T16:44:03.04+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-028d6f3ebc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-028d6f3ebc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.028d6f3ebc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:43:26.953+00:00", + "end": "2022-12-19T16:43:26.953+00:00" + }, + "recorded": "2022-12-19T16:43:26.953+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-54e37a32bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-54e37a32bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.54e37a32bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:43:10.48+00:00", + "end": "2022-12-19T16:43:10.48+00:00" + }, + "recorded": "2022-12-19T16:43:10.48+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-d24b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-d24b67f6bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.d24b67f6bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:41:23.93+00:00", + "end": "2022-12-19T16:41:23.93+00:00" + }, + "recorded": "2022-12-19T16:41:23.93+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-a94b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-a94b67f6bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.a94b67f6bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:41:22.403+00:00", + "end": "2022-12-19T16:41:22.403+00:00" + }, + "recorded": "2022-12-19T16:41:22.403+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-f1b562d2bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-f1b562d2bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.f1b562d2bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:40:27.56+00:00", + "end": "2022-12-19T16:40:27.56+00:00" + }, + "recorded": "2022-12-19T16:40:27.56+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-2d6720babb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-2d6720babb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.2d6720babb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:39:45.6+00:00", + "end": "2022-12-20T03:22:56.517+00:00" + }, + "recorded": "2022-12-20T03:22:56.517+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-b185f6a7bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-b185f6a7bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.b185f6a7bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:39:16.903+00:00", + "end": "2022-12-19T16:39:16.903+00:00" + }, + "recorded": "2022-12-19T16:39:16.903+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.74ec7d5fbb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:37:09.9+00:00", + "end": "2022-12-19T16:37:09.9+00:00" + }, + "recorded": "2022-12-19T16:37:09.9+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-1a5cf010bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-1a5cf010bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.1a5cf010bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:35:02.83+00:00", + "end": "2022-12-19T16:35:02.83+00:00" + }, + "recorded": "2022-12-19T16:35:02.83+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir/Provenance/17-f59dca04bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "17-f59dca04bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.f59dca04bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:34:40.32+00:00", + "end": "2022-12-19T16:34:40.32+00:00" + }, + "recorded": "2022-12-19T16:34:40.32+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + } + ] + }, + "agent": [ + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + }, + "actor": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + }, + "actor": { + "display": "Sweetriver" + }, + "userId": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + }, + { + "role": { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + }, + "actor": { + "display": "SystemUser" + }, + "userId": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + ] + } + } + ] +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/encoding_cda.xml b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/encoding_cda.xml new file mode 100644 index 0000000..2fb6bf6 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/encoding_cda.xml @@ -0,0 +1,46 @@ + + + + + + + + Minute Clinic Continuity of Care Document + + + + + + + + + + + George + Example + + + + + + + +
+ + + + Progress Notes + + + + War, Jo - 07/31/2007 9:02 AM EST + Smoking Status � Never Smoker Smokeless Tobacco � Not on file + + + +
+
+
+
+
\ No newline at end of file diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/hl7.txt b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/hl7.txt new file mode 100644 index 0000000..ffc2fe4 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/hl7.txt @@ -0,0 +1,22 @@ +MSH|^~\&|LAB|MYFAC|LAB||201411130917||ORU^R01|3216598|D|2.3|||AL|NE| +PID|1|ABC123DF|AND234DA_PID3|PID_4_ALTID|Smith^Patient^M||19670202 |F|||2222 22 st^^LAKE COUNTRY^NY^22222||222-222-2222|||||7890| +PV1|1|O|MYFACSOMPL||||^Smith^Patient^^^^^XAVS|||||||||||REF||SELF|||||||||||||||||||MYFAC||REG|||201411071440||||||||23390^PV1_52Smith^PV1_52Patient^H^^Dr^^PV1_52Mnemonic| +ORC|RE|PT103933301.0100|||CM|N|||201411130917|^John^Doctor^J.^^^^KYLA||^Smith^Patient^^^^^XAVS|MYFAC| +OBR|1|PT1311:H00001R301.0100|PT1311:H00001R|301.0100^Complete Blood Count (CBC)^00065227^57021-8^CBC \T\ Auto Differential^pCLOCD|R||201411130914|||KYLA||||201411130914||^Smith^Patient^^^^^XAVS||00065227||||201411130915||LAB|F||^^^^^R|^Smith^Patient^^^^^XAVS| +OBX|1|NM|301.0500^White Blood Count (WBC)^00065227^6690-2^Leukocytes^pCLOCD|1|10.1|10\S\9/L|3.1-9.7|H||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|2|NM|301.0600^Red Blood Count (RBC)^00065227^789-8^Erythrocytes^pCLOCD|1|3.2|10\S +/L|3.7-5.0|L||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|3|NM|301.0700^Hemoglobin (HGB)^00065227^718-7^Hemoglobin^pCLOCD|1|140|g/L|118-151|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|4|NM|301.0900^Hematocrit (HCT)^00065227^4544-3^Hematocrit^pCLOCD|1|0.34|L/L|0.33-0.45|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|5|NM|301.1100^MCV^00065227^787-2^Mean Corpuscular Volume^pCLOCD|1|98.0|fL|84.0-98.0|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|6|NM|301.1300^MCH^00065227^785-6^Mean Corpuscular Hemoglobin^pCLOCD|1|27.0|pg|28.3-33.5|L||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|7|NM|301.1500^MCHC^00065227^786-4^Mean Corpuscular Hemoglobin Concentration^pCLOCD|1|330|g/L|329-352|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|8|NM|301.1700^RDW^00065227^788-0^Erythrocyte Distribution Width^pCLOCD|1|12.0|%|12.0-15.0|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|9|NM|301.1900^Platelets^00065227^777-3^Platelets^pCLOCD|1|125|10\S\9/L|147-375|L||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|10|NM|301.2100^Neutrophils^00065227^751-8^Neutrophils^pCLOCD|1|8.0|10\S\9/L|1.2-6.0|H||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|11|NM|301.2300^Lymphocytes^00065227^731-0^Lymphocytes^pCLOCD|1|1.0|10\S\9/L|0.6-3.1|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|12|NM|301.2500^Monocytes^00065227^742-7^Monocytes^pCLOCD|1|1.0|10\S\9/L|0.1-0.9|H||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|13|NM|301.2700^Eosinophils^00065227^711-2^Eosinophils^pCLOCD|1|0.0|10\S\9/L|0.0-0.5|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +OBX|14|NM|301.2900^Basophils^00065227^704-7^Basophils^pCLOCD|1|0.0|10\S\9/L|0.0-0.2|N||A~S|F|||201411130916|MYFAC^MyFake Hospital^L| +ZDR||^Smith^Patient^^^^^XAVS^^^^^XX^^ATP| +ZPR|| diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/nemsis_bundle.json b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/nemsis_bundle.json new file mode 100644 index 0000000..4b99640 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/nemsis_bundle.json @@ -0,0 +1,3236 @@ +{ + "resourceType": "Bundle", + "type": "batch-response", + "entry": [ + { + "fullUrl": "https://api.rosetta.careevolution.com/Patient/35b77437-425d-419c-90b5-af4bc433ebe9", + "resource": { + "resourceType": "Patient", + "id": "35b77437-425d-419c-90b5-af4bc433ebe9", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "IheTestPatient" + }, + { + "system": "http://rosetta.careevolution.com/identifiers/Proprietary/1.3.6.1.4.1.37608", + "value": "IheTestPatient" + } + ], + "name": [ + { + "use": "official", + "family": "Smith", + "given": [ + "Patient" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "534-555-6666", + "use": "home" + } + ], + "gender": "female", + "birthDate": "1956-08-13", + "deceasedBoolean": false, + "address": [ + { + "use": "home", + "line": [ + "34 Drury Lane" + ], + "city": "Disney Land", + "state": "CA", + "postalCode": "90210" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/AllergyIntolerance/7e586cfb-0c3c-43ae-ac94-acfb26b28c91", + "resource": { + "resourceType": "AllergyIntolerance", + "id": "7e586cfb-0c3c-43ae-ac94-acfb26b28c91", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-allergyintolerance|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", + "code": "active" + }, + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/AllergyClinicalStatus", + "code": "active", + "display": "active", + "userSelected": true + } + ] + }, + "category": [ + "environment" + ], + "criticality": "high", + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "372687004", + "display": "Amoxicillin", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "372687004", + "display": "Amoxicillin (substance)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4156860", + "display": "Amoxicillin", + "userSelected": false + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "723", + "display": "amoxicillin", + "userSelected": false + } + ] + }, + "patient": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "onsetDateTime": "2008-03-10T00:00:00-04:00", + "recordedDate": "2008-03-10T00:00:00-04:00", + "reaction": [ + { + "manifestation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/AllergyReaction", + "code": "Rash", + "display": "Rash", + "userSelected": true + } + ] + } + ], + "description": "Rash" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Condition/5.4a6057fb70a64ca291b38ef98c5a7382", + "resource": { + "resourceType": "Condition", + "id": "5.4a6057fb70a64ca291b38ef98c5a7382", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + }, + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/DiagnosisStatus", + "code": "active", + "display": "active", + "userSelected": true + } + ] + }, + "verificationStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", + "code": "unconfirmed" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "55607006", + "display": "Problem", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "55607006", + "display": "Problem (finding)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4206460", + "display": "Problem", + "userSelected": false + } + ] + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.103", + "code": "0261", + "display": "Streptobacillary fever", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", + "code": "026.1", + "display": "Streptobacillary fever", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "44833220", + "display": "Streptobacillary fever", + "userSelected": false + }, + { + "system": "http://snomed.info/sct", + "code": "52138004", + "display": "Streptobacillary fever (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "A25.1", + "display": "Streptobacillosis", + "userSelected": false + } + ], + "text": "Streptobacillary fever" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "onsetDateTime": "2009-06-07T18:00:00-04:00" + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Condition/5.41e24cc8e2e44dadaa5c1b2b6d746dad", + "resource": { + "resourceType": "Condition", + "id": "5.41e24cc8e2e44dadaa5c1b2b6d746dad", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "verificationStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", + "code": "unconfirmed" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "encounter-diagnosis" + }, + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/DiagnosisType", + "code": "EncounterReason", + "display": "Encounter Reason", + "userSelected": true + } + ] + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.103", + "code": "0261", + "display": "Streptobacillary fever", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", + "code": "026.1", + "display": "Streptobacillary fever", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "44833220", + "display": "Streptobacillary fever", + "userSelected": false + }, + { + "system": "http://snomed.info/sct", + "code": "52138004", + "display": "Streptobacillary fever (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "A25.1", + "display": "Streptobacillosis", + "userSelected": false + } + ], + "text": "Streptobacillary fever" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "encounter": { + "reference": "Encounter/1bdc26a5-324a-4190-a403-b63067da1c19" + }, + "onsetDateTime": "2009-06-07T18:00:00-04:00" + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/DiagnosticReport/4.708468efc4924839b3cddb3ab5775c1f", + "resource": { + "resourceType": "DiagnosticReport", + "id": "4.708468efc4924839b3cddb3ab5775c1f", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "value": "16631200720090609061700" + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0074", + "code": "LAB" + } + ], + "text": "LAB" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "166312007", + "display": "CHEM24S", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "166312007", + "display": "Blood chemistry (procedure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4014134", + "display": "Blood chemistry", + "userSelected": false + } + ], + "text": "CHEM24S" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-09T18:17:00-04:00", + "issued": "2009-06-09T18:17:00-04:00", + "result": [ + { + "reference": "Observation/2.898b46638a8a4f0f8ad5faeca51f381b" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/DiagnosticReport/4.9a8ce5a0c44d4affb569cf64ed82dbb2", + "resource": { + "resourceType": "DiagnosticReport", + "id": "4.9a8ce5a0c44d4affb569cf64ed82dbb2", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "value": "2660400720090608120900" + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0074", + "code": "LAB" + } + ], + "text": "LAB" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "26604007", + "display": "CBC", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "26604007", + "display": "Complete blood count (procedure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4132152", + "display": "Complete blood count", + "userSelected": false + } + ], + "text": "CBC" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "result": [ + { + "reference": "Observation/2.82f0c47ab5274b738bd8bcd178595ed1" + }, + { + "reference": "Observation/2.862f583179e347038c75895ceac08750" + }, + { + "reference": "Observation/2.3a6b507900634284a6c8edf7a974e65d" + }, + { + "reference": "Observation/2.48172d29edee4125ac8f975e7c6f9cf8" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/DiagnosticReport/4.19f34e64dac6462fa2e86113eacd32c8", + "resource": { + "resourceType": "DiagnosticReport", + "id": "4.19f34e64dac6462fa2e86113eacd32c8", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "value": "Operative20090607050000" + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0074", + "code": "LAB" + } + ], + "text": "LAB" + } + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/LabService", + "code": "Operative", + "display": "Operative", + "userSelected": true + } + ], + "text": "Operative" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-07T17:00:00-04:00", + "issued": "2009-06-07T17:00:00-04:00", + "result": [ + { + "reference": "Observation/2.a346355cd2524a6686aeecd35e7f4aab" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/DiagnosticReport/4.822796b740bb446791ef4e77d0756f42", + "resource": { + "resourceType": "DiagnosticReport", + "id": "4.822796b740bb446791ef4e77d0756f42", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "value": "7930100820090608120900" + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0074", + "code": "LAB" + } + ], + "text": "LAB" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "79301008", + "display": "Electrolytes measurement (procedure)", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "79301008", + "display": "Electrolytes measurement (procedure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4193783", + "display": "Electrolytes measurement", + "userSelected": false + } + ], + "text": "Electrolytes measurement (procedure)" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "result": [ + { + "reference": "Observation/2.1d376f381e404f629a4b668eb4bf7d72" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Encounter/1bdc26a5-324a-4190-a403-b63067da1c19", + "resource": { + "resourceType": "Encounter", + "id": "1bdc26a5-324a-4190-a403-b63067da1c19", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "system": "http://rosetta.careevolution.com/encounteridentifiers", + "value": "07f8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "in-progress", + "class": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#coding", + "valueCoding": { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/PatientClass", + "code": "Inpatient", + "display": "Inpatient", + "userSelected": true + } + }, + { + "url": "http://careevolution.com/fhirextensions#coding", + "valueCoding": { + "system": "https://athena.ohdsi.org/", + "code": "9201", + "display": "Inpatient Visit", + "userSelected": false + } + } + ], + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "participant": [ + { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#dataSource", + "extension": [ + { + "url": "code", + "valueString": "cj51SkDNLEqvzVeIR5m1jQ==" + }, + { + "url": "name", + "valueString": "Custodian: Community Health Information Exchange;Author: Community Health Information Exchange;" + } + ] + } + ], + "type": [ + { + "coding": [ + { + "system": "http://careevolution.com/fhircodes#CaregiverRelationshipType", + "code": "ScheduledCaregiver", + "display": "Scheduled Caregiver", + "userSelected": true + } + ] + } + ], + "period": { + "start": "2004-05-06T00:00:00-04:00" + }, + "individual": { + "reference": "Practitioner/a0ba6517-af46-44e6-8359-d7297d27a763" + } + } + ], + "period": { + "start": "2009-06-07T17:00:00-04:00" + }, + "diagnosis": [ + { + "condition": { + "reference": "Condition/5.41e24cc8e2e44dadaa5c1b2b6d746dad" + }, + "use": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/diagnosis-role", + "code": "CC" + }, + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/DiagnosisType", + "code": "EncounterReason", + "display": "Encounter Reason", + "userSelected": true + } + ] + } + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Encounter/3cc0c5b3-5e61-4c2d-b2f5-6d3a77b438d2", + "resource": { + "resourceType": "Encounter", + "id": "3cc0c5b3-5e61-4c2d-b2f5-6d3a77b438d2", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "system": "http://rosetta.careevolution.com/encounteridentifiers", + "value": "08f8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "in-progress", + "class": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#coding", + "valueCoding": { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/PatientClass", + "code": "PCPVisit", + "display": "PCP Visit", + "userSelected": true + } + }, + { + "url": "http://careevolution.com/fhirextensions#coding", + "valueCoding": { + "system": "https://athena.ohdsi.org/", + "code": "9202", + "display": "Outpatient Visit", + "userSelected": false + } + } + ], + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "period": { + "start": "2009-08-10T22:00:00-04:00" + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Practitioner/a0ba6517-af46-44e6-8359-d7297d27a763", + "resource": { + "resourceType": "Practitioner", + "id": "a0ba6517-af46-44e6-8359-d7297d27a763", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner|3.1.1", + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Practitioner|2.0" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_CareEvolution" + }, + "identifier": [ + { + "use": "usual", + "system": "http://rosetta.careevolution.com/identifiers/CareEvolution/CaregiverIdentifier/1.3.6.1.4.1.37608_CareEvolution", + "value": "7v1bjE+iv1TpK7xwyhaXbw==" + }, + { + "system": "http://rosetta.careevolution.com/identifiers/Proprietary/1.3.6.1.4.1.37608", + "value": "c68394cb-57ad-e411-8260-0050b664cec5" + } + ], + "name": [ + { + "use": "official", + "family": "Smith", + "given": [ + "David", + "K" + ], + "suffix": [ + "MD" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "212-555-7351", + "use": "home" + } + ], + "address": [ + { + "use": "home", + "line": [ + "543 Doctor Avenue" + ], + "city": "New York", + "state": "NY", + "postalCode": "10001" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationAdministration/d07d711c-d897-410a-a3da-d605e5ed1a58", + "resource": { + "resourceType": "MedicationAdministration", + "id": "d07d711c-d897-410a-a3da-d605e5ed1a58", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#medicationAdministration-scheduledTime", + "valueDateTime": "2009-08-11T22:00:00-04:00" + } + ], + "identifier": [ + { + "use": "usual", + "value": "1cf8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "C0981193", + "display": "Acetaminophen 325 mg oral tablet", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + "display": "acetaminophen 325 MG Oral Tablet", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "1127433", + "display": "acetaminophen 325 MG Oral Tablet", + "userSelected": false + } + ], + "text": "Acetaminophen 325 mg oral tablet" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectivePeriod": { + "start": "2009-08-11T22:00:00-04:00", + "end": "2009-08-11T22:00:00-04:00" + }, + "request": { + "reference": "MedicationRequest/7f7348c8-7377-41ba-9628-5a68aa34ceec" + }, + "dosage": { + "dose": { + "value": -1, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationAdministration/b5def510-50f4-4efc-988c-a7e6e74ef61c", + "resource": { + "resourceType": "MedicationAdministration", + "id": "b5def510-50f4-4efc-988c-a7e6e74ef61c", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#medicationAdministration-scheduledTime", + "valueDateTime": "2009-08-11T17:00:00-04:00" + } + ], + "identifier": [ + { + "use": "usual", + "value": "1df8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "C0973886", + "display": "Albuterol 0.09mg/actuat inhalant solution", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "329498", + "display": "albuterol 0.09 MG/ACTUAT", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "1154459", + "display": "albuterol 0.09 MG/ACTUAT", + "userSelected": false + } + ], + "text": "Albuterol 0.09mg/actuat inhalant solution" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectivePeriod": { + "start": "2009-08-11T17:00:00-04:00", + "end": "2009-08-11T17:00:00-04:00" + }, + "request": { + "reference": "MedicationRequest/90a1faa3-43f8-4565-b170-cc4025cab2db" + }, + "dosage": { + "route": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.5.112", + "code": "PO", + "display": "PO", + "userSelected": true + } + ] + }, + "dose": { + "value": -1, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationAdministration/e33653e5-387a-498e-acde-14cce4ddac78", + "resource": { + "resourceType": "MedicationAdministration", + "id": "e33653e5-387a-498e-acde-14cce4ddac78", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#medicationAdministration-scheduledTime", + "valueDateTime": "2009-08-12T17:00:00-04:00" + } + ], + "identifier": [ + { + "use": "usual", + "value": "1ef8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/MedicationProfileMedication", + "code": "C3243", + "display": "Saline", + "userSelected": true + } + ], + "text": "Saline" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectivePeriod": { + "start": "2009-08-12T17:00:00-04:00", + "end": "2009-08-12T17:00:00-04:00" + }, + "request": { + "reference": "MedicationRequest/1d1e84cd-5b15-4123-b21a-3125ef6ad192" + }, + "dosage": { + "route": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.2.16.840.1.113883.3.26.1.1/MedicationAdministrationRoute", + "code": "C38216", + "display": "Respiratory (Inhalation)", + "userSelected": true + } + ] + }, + "dose": { + "value": -1, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "mL" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationRequest/90a1faa3-43f8-4565-b170-cc4025cab2db", + "resource": { + "resourceType": "MedicationRequest", + "id": "90a1faa3-43f8-4565-b170-cc4025cab2db", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "PLAC" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "1df8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "intent": "order", + "reportedBoolean": false, + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "C0973886", + "display": "Albuterol 0.09mg/actuat inhalant solution", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "329498", + "display": "albuterol 0.09 MG/ACTUAT", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "1154459", + "display": "albuterol 0.09 MG/ACTUAT", + "userSelected": false + } + ], + "text": "Albuterol 0.09mg/actuat inhalant solution" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "authoredOn": "2009-08-11T17:00:00-04:00", + "requester": { + "reference": "Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd" + }, + "dosageInstruction": [ + { + "text": "Albuterol 0.09mg/actuat inhalant solution", + "timing": { + "event": [ + "2009-08-11T17:00:00-04:00" + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/OrderFrequency", + "code": "Unspecified", + "display": "Unspecified", + "userSelected": true + } + ] + } + }, + "asNeededBoolean": false, + "route": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.5.112", + "code": "PO", + "display": "PO", + "userSelected": true + } + ] + }, + "doseAndRate": [ + { + "doseRange": { + "low": { + "value": 5.5, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + }, + "high": { + "value": 5.5, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + } + } + } + ] + } + ], + "dispenseRequest": { + "validityPeriod": { + "start": "2009-08-11T17:00:00-04:00", + "end": "2009-08-11T17:00:00-04:00" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationRequest/72c3742d-ec08-489f-906a-bd039a8c0d07", + "resource": { + "resourceType": "MedicationRequest", + "id": "72c3742d-ec08-489f-906a-bd039a8c0d07", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "PLAC" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "2ef8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "intent": "order", + "reportedBoolean": false, + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "197454", + "display": "Cephalexin 500 MG oral tablet", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "197454", + "display": "cephalexin 500 MG Oral Tablet", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "1786652", + "display": "cephalexin 500 MG Oral Tablet", + "userSelected": false + } + ], + "text": "Cephalexin 500 MG oral tablet" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "authoredOn": "2009-06-10T16:00:00-04:00", + "requester": { + "reference": "Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd" + }, + "dosageInstruction": [ + { + "text": "Cephalexin 500 MG oral tablet", + "timing": { + "event": [ + "2009-06-10T16:00:00-04:00" + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/OrderFrequency", + "code": "Unspecified", + "display": "Unspecified", + "userSelected": true + } + ] + } + }, + "asNeededBoolean": false + } + ], + "dispenseRequest": { + "validityPeriod": { + "start": "2009-06-10T16:00:00-04:00" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationRequest/44ad877e-e2de-45bd-8981-b13b3de29458", + "resource": { + "resourceType": "MedicationRequest", + "id": "44ad877e-e2de-45bd-8981-b13b3de29458", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "PLAC" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "2df8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "intent": "order", + "reportedBoolean": false, + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "309362", + "display": "Clopidogrel 75 MG oral tablet", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "309362", + "display": "clopidogrel 75 MG Oral Tablet", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "19075601", + "display": "clopidogrel 75 MG Oral Tablet", + "userSelected": false + } + ], + "text": "Clopidogrel 75 MG oral tablet" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "authoredOn": "2009-08-15T09:00:00-04:00", + "requester": { + "reference": "Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd" + }, + "dosageInstruction": [ + { + "text": "Clopidogrel 75 MG oral tablet", + "timing": { + "event": [ + "2009-08-15T09:00:00-04:00" + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/OrderFrequency", + "code": "Unspecified", + "display": "Unspecified", + "userSelected": true + } + ] + } + }, + "asNeededBoolean": false + } + ], + "dispenseRequest": { + "validityPeriod": { + "start": "2009-08-15T09:00:00-04:00" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationRequest/7f7348c8-7377-41ba-9628-5a68aa34ceec", + "resource": { + "resourceType": "MedicationRequest", + "id": "7f7348c8-7377-41ba-9628-5a68aa34ceec", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "PLAC" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "1cf8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "intent": "order", + "reportedBoolean": false, + "medicationCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.88", + "code": "C0981193", + "display": "Acetaminophen 325 mg oral tablet", + "userSelected": true + }, + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "313782", + "display": "acetaminophen 325 MG Oral Tablet", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "1127433", + "display": "acetaminophen 325 MG Oral Tablet", + "userSelected": false + } + ], + "text": "Acetaminophen 325 mg oral tablet" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "authoredOn": "2009-08-11T22:00:00-04:00", + "requester": { + "reference": "Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd" + }, + "dosageInstruction": [ + { + "text": "Acetaminophen 325 mg oral tablet", + "timing": { + "event": [ + "2009-08-11T22:00:00-04:00" + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/OrderFrequency", + "code": "Unspecified", + "display": "Unspecified", + "userSelected": true + } + ] + } + }, + "asNeededBoolean": false, + "doseAndRate": [ + { + "doseRange": { + "low": { + "value": 325, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + }, + "high": { + "value": 325, + "unit": "milligram", + "system": "http://unitsofmeasure.org", + "code": "mg" + } + } + } + ] + } + ], + "dispenseRequest": { + "validityPeriod": { + "start": "2009-08-11T22:00:00-04:00", + "end": "2009-08-11T22:00:00-04:00" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/MedicationRequest/1d1e84cd-5b15-4123-b21a-3125ef6ad192", + "resource": { + "resourceType": "MedicationRequest", + "id": "1d1e84cd-5b15-4123-b21a-3125ef6ad192", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "PLAC" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "1ef8cd8b-58ad-e411-8260-0050b664cec5" + } + ], + "status": "completed", + "intent": "order", + "reportedBoolean": false, + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/OrderableItem", + "code": "C3243", + "display": "Saline", + "userSelected": true + } + ], + "text": "Saline" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "authoredOn": "2009-08-12T17:00:00-04:00", + "requester": { + "reference": "Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd" + }, + "dosageInstruction": [ + { + "text": "Saline", + "timing": { + "event": [ + "2009-08-12T17:00:00-04:00" + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/OrderFrequency", + "code": "Unspecified", + "display": "Unspecified", + "userSelected": true + } + ] + } + }, + "asNeededBoolean": false, + "route": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.2.16.840.1.113883.3.26.1.1/OrderRoute", + "code": "C38216", + "display": "Respiratory (Inhalation)", + "userSelected": true + } + ] + }, + "doseAndRate": [ + { + "doseRange": { + "low": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "mL" + }, + "high": { + "value": 0.5, + "unit": "milliliter", + "system": "http://unitsofmeasure.org", + "code": "mL" + } + } + } + ] + } + ], + "dispenseRequest": { + "validityPeriod": { + "start": "2009-08-12T17:00:00-04:00", + "end": "2009-08-12T17:00:00-04:00" + } + } + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Practitioner/a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd", + "resource": { + "resourceType": "Practitioner", + "id": "a75b0d51-7464-4e9a-9c6c-d3a7488fd2bd", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner|3.1.1", + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Practitioner|2.0" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_CareEvolution" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#practitioner-role", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary/CaregiverType", + "code": "Prescriber", + "display": "Prescriber", + "userSelected": true + } + ] + } + } + ], + "identifier": [ + { + "use": "usual", + "system": "http://rosetta.careevolution.com/identifiers/CareEvolution/CaregiverIdentifier/1.3.6.1.4.1.37608_CareEvolution", + "value": "Unknown" + } + ], + "name": [ + { + "use": "official", + "family": "Unknown" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.898b46638a8a4f0f8ad5faeca51f381b", + "resource": { + "resourceType": "Observation", + "id": "2.898b46638a8a4f0f8ad5faeca51f381b", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.708468efc4924839b3cddb3ab5775c1f" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "275790008", + "display": "CHLORIDE", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "275790008", + "display": "Chloride in sample (finding)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4077512", + "display": "Chloride in sample", + "userSelected": false + } + ], + "text": "CHLORIDE" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-09T18:17:00-04:00", + "issued": "2009-06-09T18:17:00-04:00", + "valueString": "112 MMOL/L", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "H", + "display": "H", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "H", + "display": "High", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 98, + "unit": "MMOL/L" + }, + "high": { + "value": 107, + "unit": "MMOL/L" + }, + "text": "98-107 MMOL/L" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.3a6b507900634284a6c8edf7a974e65d", + "resource": { + "resourceType": "Observation", + "id": "2.3a6b507900634284a6c8edf7a974e65d", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.9a8ce5a0c44d4affb569cf64ed82dbb2" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "16378004", + "display": "PLT", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "16378004", + "display": "Platelet (cell structure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4039428", + "display": "Platelet", + "userSelected": false + } + ], + "text": "PLT" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "valueString": "132 K/CUMM", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "L", + "display": "L", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "L", + "display": "Low", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 150, + "unit": "K/CUMM" + }, + "high": { + "value": 450, + "unit": "K/CUMM" + }, + "text": "150-450 K/CUMM" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.a346355cd2524a6686aeecd35e7f4aab", + "resource": { + "resourceType": "Observation", + "id": "2.a346355cd2524a6686aeecd35e7f4aab", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.19f34e64dac6462fa2e86113eacd32c8" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/LabObservationType", + "code": "Operative", + "display": "Operative", + "userSelected": true + } + ], + "text": "OPERATIVE NOTE\nDICTATED BY: Jack Blue, MD\nDICTATED: 6/7/2009 8:03 P 000087489\nTRANSCRIBED: 6/8/2009 1:06 A vl D\nPATIENT NAME: Test, Ihe\nHEALTH RECORD NO.: 123456783\nBILLING NO.: 88888888\nROOM N" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-07T17:00:00-04:00", + "issued": "2009-06-07T17:00:00-04:00", + "valueString": "OPERATIVE NOTE\nDICTATED BY: Jack Blue, MD\nDICTATED: 6/7/2009 8:03 P 000087489\nTRANSCRIBED: 6/8/2009 1:06 A vl D\nPATIENT NAME: Test, Ihe\nHEALTH RECORD NO.: 123456783\nBILLING NO.: 88888888\nROOM NO.: SIC\nDate of Procedure: 11/28/06\nPREOPERATIVE DIAGNOSES: Aortic stenosis and mitral\ninsufficiency.\nPOSTOPERATIVE DIAGNOSES: Aortic stenosis and mitral\ninsufficiency.\nPROCEDURE PERFORMED: Aortic valve replacement with a 21 mm\nEdwards Magna bovine pericardial bioprosthesis and a reverse\nsaphenous vein graft to the right coronary.\nSURGEON: Dr. Blue.\nASSISTANT: Pat Yellow, CST.\nANESTHESIA: General tracheal anesthesia, Dr. Green.\nAortic cross clamp time was 117 minutes.\nCardiopulmonary bypass time was 168 minutes.\nAtrial and ventricular pacing wires.\nDrains - 32 French mediastinal chest tube, 28 French\nmediastinal chest tube.\nFINDINGS: Good saphenous vein conduit, normal ascending aorta,\naortic valve was trileaflet with heavy calcification to the\nleaflets. The annulus was not too heavily calcified with\nmoderate aortic annular calcification. Also noted was a right\ncoronary ostial intimal flap that was noted after I was\ngetting ready to seat the valve and this will be described in\nthe text of the operation; therefore the right coronary was\ngrafted. The right coronary was a 2.5 mm vessel with no\ndistal disease.\nINDICATIONS FOR PROCEDURE: The patient is a 73-year-old woman\nwho was found to have known aortic stenosis and found to have\nbreast carcinoma needing resection of her breast cancer;\nhowever, has had severe aortic stenosis prior to undergoing\nbreast cancer surgery. She needed her heart fixed. The\npatient is brought to the operating room for aortic valve\nreplacement. Preoperative echocardiogram demonstrates severe\naortic stenosis and moderate aortic insufficiency and 2+\nmitral insufficiency. Intraoperative transesophageal\nechocardiogram was performed by Dr. Mauve.\nDESCRIPTION OF THE PROCEDURE: The patient was brought to the\noperating room and placed in a comfortable supine position on\nthe operating table. She underwent induction of general\nendotracheal anesthesia and placement of invasive monitor,\nlines and intraoperative echocardiogram by Dr. Mauve.\nEchocardiogram demonstrated severe aortic stenosis and heavily\ncalcified leaflets with moderate mitral insufficiency given\nher age. It was my impression that decompressing her outflow\ntract would improve her mitral valve insufficiency. The\npatient was then positioned, prepped and draped in the usual\nsterile fashion. A median sternotomy was accomplished. The\nchest was opened and a mediastinal dissection proceeded. The\npericardium was opened and suspended. The patient was\nheparinized. The distal ascending aorta was cannulated\nthrough double aortic pursestring sutures. A two stage venous\ncannula was positioned to the right atrial appendage\npursestring and the patient was initiated on cardiopulmonary\nbypass. Antegrade and retrograde cardioplegia cannulas were\npositioned. An LV sump cannula was positioned through right\nsuperior pulmonary vein. The patient was cooled to 32 degrees\ncentigrade. The aorta was cross clamped and antegrade and\nretrograde cold blood cardioplegia was administered. After\ndiastolic rest of the heart we made a transverse aortotomy\nwith a hockey stick extension down into the noncoronary sinus.\nExposure of the valve was actually pretty descent. Valve\nfindings as noted previously. The valve was sharply excised.\nThe majority of the annular debridement was done fortunately\nwith the scissors; i.e., the majority of the annular\ncalcifications were in the leaflets and not so much in the\nannulus. Once we got the leaflets cut out the annulus was d\nbrided of the remainder of the calcific debris. I incised it\nprior to placement of sutures and it sized out to 21 mm sized\nout very easily and in fact a 23 almost fit. I then placed\nmultiple annular sutures circumferentially about the annulus\nthat was consistent with interrupted 2-0 Ethibond horizontal\nmattress pledgeted sutures with pledgets on the ventricular\nside. Once we got all our sutures in, I resized the 21 mm\nsizer. A 21 interannular sizer was quit snug; however, the\nsizer on the supra-annular position looked quite comfortable.\nA 21 mm valve was brought up to the field and it was washed\nand then brought to operative field. While I was examining\nthe root of the heart I noted that there an intimal dissection\nof the ostium of the right coronary that is completely\nunexplainable. There was no direct cannulation of the right\ncoronary ostium. There was no mechanical debridement at the\nright coronary ostium but fortuitously identified the right\ncoronary intimal tear and it was an intimal tear of the aortic\nwall and it extended to the right coronary ostium. I tacked\nthis intima down to the aortic wall with interrupted\nhorizontal mattress 5-0 Prolene suture but I was very\nuncomfortable just simply leaving this in place that it posed\nthe possibility for acute right coronary dissection, RV\ninfarct, and inferior wall infarct postoperatively. Therefore,\neven though she did not have any coronary disease, I was very\nconcerned about leaving her with an unprotected right\ncoronary. I therefore asked Pat Yellow, my assistant,\nto scrub and take a segment of vein from the left lower leg\ngreater saphenous vein was harvested and the wound was closed\nin layers with absorbable suture. While she was taking vein,\nI placed annular sutures through the valve, sewing ring the\nvalve and then seated down into the annulus and it seated\nnicely. It was seated first in the left coronary sinus\nfollowed by the right coronary sinus followed by the\nnoncoronary sinus. The remainder of the sutures was tied\ncircumferentially. I examined the valve and it appeared to be\nseated well and I was very pleased with the seating.\nPat Yellow was still procuring vein when I was closing\nthe aortotomy. The aortotomy was closed in a two layer\nfashion with running 4-0 Prolene in a two layer fashion. The\nfirst layer was a running horizontal mattress with 4-0 Prolene\nfollowed by over and over 4-0 Prolene. By this time the vein\nwas out. I identified a spot in the mid right coronary about\nthe level of the acute margin, made an arteriotomy and sewed\nthe vein graft end-to-side with running 7-0 Prolene suture. I\nmade a single 4.4 mm aortotomy distal to my transverse\naortotomy and anastomosed the vein graft end-to-side with\nrunning 5-0 Prolene suture. The patient was rewarmed while I\nreconstructed the last anastomosis and a warm dose of\ncardioplegia was given through the retrograde cannula. The\npatient was positioned in Trendelenburg position. The\nventricle and aorta were de-aired out the ascending aorta.\nThe patient was positioned head down position. The aortic\ncross clamp was removed. The patient's rhythm returned to\nventricular fibrillation. I defibrillated the heart several\ntimes with loading dose of Lidocaine and amiodarone and was\nfinally able to get her defibrillated to a paced rhythm. The\ngraft was de-aired and the bulldog was released. The\naortotomy was hemostatic. Atrial and ventricular pacing wires\nwere positioned and mediastinal drains were positioned and\nproximal graft markers were positioned. With satisfactory\nreperfusion time, we went ahead and ventilated the patient,\nremoved the retrograde cannula was previously removed. The LV\nsump cannula was removed once I had the heart beating. We\nthen proceeded with echocardiogram interrogation.\nEchocardiogram interrogation demonstrated satisfactory\nde-airing and satisfactory valve function with no aortic\ninsufficiency and only trivial mitral insufficiency. With\nthat result, we removed the antegrade cardioplegia needle and\nthen weaned and separated from cardiopulmonary bypass on low\ndose inotropic support. The venous cannula was removed. The\npump line was administered through the aortic cannula with\nsatisfactory hemodynamics. Protamine was administered. We\ngained hemostasis through the mediastinum. The aorta was\nde-cannulated. Pursestring sutures were secured. With\nsatisfactory hemostasis we preceded closure. The sternum was\napproximated with interrupted #7 stainless steel wires. The\ndeep fascia was closed with running #1 PDS. The subcutaneous\ntissue was closed with 2-0 Vicryl. The skin edges were closed\nwith 3-0 Monocryl. The patient's wounds were dressed in a\ndry, sterile dressing. The patient tolerated the procedure\nwell with no operative complications. The patient was\nreturned to the surgical intensive care unit in critical\ncondition.\n___________________________\nJack Blue, MD\ncc: Donald M Red, MD\n Jack Blue, MD\n Chip Orange, MD\nDOCUMENT #: 2487825", + "referenceRange": [ + { + "text": "unknown" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.82f0c47ab5274b738bd8bcd178595ed1", + "resource": { + "resourceType": "Observation", + "id": "2.82f0c47ab5274b738bd8bcd178595ed1", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.9a8ce5a0c44d4affb569cf64ed82dbb2" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "767002", + "display": "WBC", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "767002", + "display": "White blood cell count (procedure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4298431", + "display": "White blood cell count", + "userSelected": false + } + ], + "text": "WBC" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "valueString": "16.6 K/CUMM", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "H", + "display": "H", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "H", + "display": "High", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 4, + "unit": "K/CUMM" + }, + "high": { + "value": 10.5, + "unit": "K/CUMM" + }, + "text": "4.0-10.5 K/CUMM" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.48172d29edee4125ac8f975e7c6f9cf8", + "resource": { + "resourceType": "Observation", + "id": "2.48172d29edee4125ac8f975e7c6f9cf8", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.9a8ce5a0c44d4affb569cf64ed82dbb2" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "28317006", + "display": "HCT", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "28317006", + "display": "Hematocrit determination (procedure)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4151358", + "display": "Hematocrit determination", + "userSelected": false + } + ], + "text": "HCT" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "valueString": "28.6 %", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "L", + "display": "L", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "L", + "display": "Low", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 37, + "unit": "%", + "system": "http://unitsofmeasure.org", + "code": "%" + }, + "high": { + "value": 47, + "unit": "%", + "system": "http://unitsofmeasure.org", + "code": "%" + }, + "text": "37.0-47.0 %" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.862f583179e347038c75895ceac08750", + "resource": { + "resourceType": "Observation", + "id": "2.862f583179e347038c75895ceac08750", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.9a8ce5a0c44d4affb569cf64ed82dbb2" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "38082009", + "display": "HGB", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "38082009", + "display": "Hemoglobin (substance)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4244232", + "display": "Hemoglobin", + "userSelected": false + } + ], + "text": "HGB" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "valueString": "10.2 G/DL", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "L", + "display": "L", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "L", + "display": "Low", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 12.5, + "unit": "G/DL" + }, + "high": { + "value": 16, + "unit": "G/DL" + }, + "text": "12.5-16.0 G/DL" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Observation/2.1d376f381e404f629a4b668eb4bf7d72", + "resource": { + "resourceType": "Observation", + "id": "2.1d376f381e404f629a4b668eb4bf7d72", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-reportReference", + "valueReference": { + "reference": "DiagnosticReport/4.822796b740bb446791ef4e77d0756f42" + } + } + ], + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory" + } + ], + "text": "laboratory" + } + ], + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.96", + "code": "31811003", + "display": "CARBON DIOXIDE", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "31811003", + "display": "Carbon dioxide (substance)", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "4135927", + "display": "Carbon dioxide", + "userSelected": false + } + ], + "text": "CARBON DIOXIDE" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "effectiveDateTime": "2009-06-08T12:09:00-04:00", + "issued": "2009-06-08T12:09:00-04:00", + "valueString": "26 MMOL/L", + "interpretation": [ + { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.DemoNamespace/Acuity", + "code": "N", + "display": "N", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/v3/ObservationInterpretation", + "code": "N", + "display": "Normal", + "userSelected": false + } + ] + } + ], + "referenceRange": [ + { + "low": { + "value": 22, + "unit": "MMOL/L" + }, + "high": { + "value": 31, + "unit": "MMOL/L" + }, + "text": "22-31 MMOL/L" + } + ] + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Procedure/7.bc640751160d4dce960769e09f6a7de1", + "resource": { + "resourceType": "Procedure", + "id": "7.bc640751160d4dce960769e09f6a7de1", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "status": "completed", + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.12", + "code": "3227", + "display": "Bronchoscopic bronchial thermoplasty, ablation of airway smooth muscle", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/sid/icd-9-cm/procedure", + "code": "32.27", + "display": "Bronchoscopic bronchial thermoplasty, ablation of airway smooth muscle", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "40756810", + "display": "Bronchoscopic bronchial thermoplasty, ablation of airway smooth muscle", + "userSelected": false + } + ], + "text": "Bronchoscopic bronchial thermoplasty, ablation of airway smooth muscle" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "performedDateTime": "2009-08-10T23:00:00-04:00" + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Procedure/7.93805cca981542d7a67e487f0b94944a", + "resource": { + "resourceType": "Procedure", + "id": "7.93805cca981542d7a67e487f0b94944a", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "status": "completed", + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.12", + "code": "0396", + "display": "Percutaneous denervation of facet", + "userSelected": true + }, + { + "system": "http://hl7.org/fhir/sid/icd-9-cm/procedure", + "code": "03.96", + "display": "Percutaneous denervation of facet", + "userSelected": false + }, + { + "system": "https://athena.ohdsi.org/", + "code": "2000195", + "display": "Percutaneous denervation of facet", + "userSelected": false + }, + { + "system": "http://snomed.info/sct", + "code": "50055008", + "display": "Percutaneous denervation of facet (procedure)", + "userSelected": false + } + ], + "text": "Percutaneous denervation of facet" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "performedDateTime": "2009-06-07T20:00:00-04:00" + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Procedure/7.2ed0fe30094a46e7bf6b3ebe69ead24a", + "resource": { + "resourceType": "Procedure", + "id": "7.2ed0fe30094a46e7bf6b3ebe69ead24a", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "status": "completed", + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.103", + "code": "465", + "display": "Treatment for Upper Respiratory Infection", + "userSelected": true + } + ], + "text": "Treatment for Upper Respiratory Infection" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "performedDateTime": "2009-07-01T12:00:00-04:00" + } + }, + { + "fullUrl": "https://api.rosetta.careevolution.com/Procedure/7.14e26bb09d7649b2a6ce10f794ca8960", + "resource": { + "resourceType": "Procedure", + "id": "7.14e26bb09d7649b2a6ce10f794ca8960", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure|3.1.1" + ], + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "status": "completed", + "code": { + "coding": [ + { + "system": "http://rosetta.careevolution.com/codes/Proprietary.CareEvolution/ProcedureCode", + "code": "OtherThymusOperations", + "display": "Oth thorac op thymus NOS", + "userSelected": true + } + ], + "text": "Other and unspecified thoracoscopic operations on thymus" + }, + "subject": { + "reference": "Patient/35b77437-425d-419c-90b5-af4bc433ebe9" + }, + "performedDateTime": "2009-06-07T20:30:00-04:00" + } + }, + { + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "warning", + "code": "processing", + "details": { + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAProcessingMessage", + "code": "CdaParsingWarning" + } + ], + "text": "Labs: Missing description in the reference dictionary for #labgroup_3. Line Number - 662, XPath - ClinicalDocument/component/structuredBody/component/section/entry/organizer/code/originalText/reference" + } + }, + { + "severity": "warning", + "code": "processing", + "details": { + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAProcessingMessage", + "code": "CdaParsingWarning" + } + ], + "text": "Labs: Missing description in the reference dictionary for #labgroup_5. Line Number - 701, XPath - ClinicalDocument/component/structuredBody/component/section/entry/organizer/code/originalText/reference" + } + }, + { + "severity": "warning", + "code": "processing", + "details": { + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAProcessingMessage", + "code": "CdaParsingWarning" + } + ], + "text": "Labs: Missing description in the reference dictionary for #labgroup_10. Line Number - 821, XPath - ClinicalDocument/component/structuredBody/component/section/entry/organizer/code/originalText/reference" + } + }, + { + "severity": "warning", + "code": "processing", + "details": { + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAProcessingMessage", + "code": "CdaParsingWarning" + } + ], + "text": "Labs: Missing description in the reference dictionary for #reportheader_12. Line Number - 860, XPath - ClinicalDocument/component/structuredBody/component/section/entry/organizer/code/originalText/reference" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Encounters" + } + ], + "text": "Encounters:\nTotal: 2\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 3 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Reports" + } + ], + "text": "Reports:\nTotal: 3\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 1 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Problems" + } + ], + "text": "Problems:\nTotal: 1\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 1 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Allergies" + } + ], + "text": "Allergies:\nTotal: 1\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 7 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Labs" + } + ], + "text": "Labs:\nTotal: 7\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 5 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Medications" + } + ], + "text": "Medications:\nTotal: 5\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 4 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Procedures" + } + ], + "text": "Procedures:\nTotal: 4\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#Total", + "valueInteger": 1 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#InvalidEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#DuplicateEntries", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Section#IgnoredEntries", + "valueInteger": 1 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDASectionSummary", + "code": "Vitals" + } + ], + "text": "Vitals:\nTotal: 1\nInvalidEntries: 0\nDuplicateEntries: 0\nIgnoredEntries: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Document#UnstructuredDocument", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Document#MissingEncompassingEncounter", + "valueBoolean": true + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Document#MissingLegalAuthenticator", + "valueBoolean": true + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Document#MissingSignatureCode", + "valueBoolean": true + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Document" + } + ], + "text": "Document:\nUnstructuredDocument: False\nMissingEncompassingEncounter: True\nMissingLegalAuthenticator: True\nMissingSignatureCode: True" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingGender", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingRace", + "valueBoolean": true + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingEthnicity", + "valueBoolean": true + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingDateOfBirth", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientStreetAddress", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientAddressCity", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientAddressState", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientAddressPostalCode", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientPhoneNumber", + "valueBoolean": false + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#MissingPatientEMailAddress", + "valueBoolean": true + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Patient#ByIdentifierType", + "valueQuantity": { + "value": 1, + "unit": "1.3.6.1.4.1.37608", + "code": "1.3.6.1.4.1.37608" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Patient" + } + ], + "text": "Patient:\nMissingGender: False\nMissingRace: True\nMissingEthnicity: True\nMissingDateOfBirth: False\nMissingPatientStreetAddress: False\nMissingPatientAddressCity: False\nMissingPatientAddressState: False\nMissingPatientAddressPostalCode: False\nMissingPatientPhoneNumber: False\nMissingPatientEMailAddress: True\nByIdentifierType 1.3.6.1.4.1.37608: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#Total", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#MissingDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#MissingStopDate", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#MissingStopDateByStatus", + "valueQuantity": { + "value": 1, + "unit": "MissingStatus", + "code": "MissingStatus" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#MissingStopDateByStatus", + "valueQuantity": { + "value": 1, + "unit": "active:active", + "code": "active:active" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#ByType", + "valueQuantity": { + "value": 1, + "unit": "EncounterReason:Encounter Reason", + "code": "EncounterReason:Encounter Reason" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#ByType", + "valueQuantity": { + "value": 1, + "unit": "55607006:Problem", + "code": "55607006:Problem" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#ByStatus", + "valueQuantity": { + "value": 1, + "unit": "MissingStatus", + "code": "MissingStatus" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#ByStatus", + "valueQuantity": { + "value": 1, + "unit": "active:active", + "code": "active:active" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Problems#BySourceCodingSystem", + "valueQuantity": { + "value": 2, + "unit": "2.16.840.1.113883.6.103", + "code": "2.16.840.1.113883.6.103" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Problems" + } + ], + "text": "Problems:\nTotal: 2\nMissingDate: 0\nMissingStopDate: 2\nMissingStopDateByStatus MissingStatus: 1\nMissingStopDateByStatus active:active: 1\nByType EncounterReason:Encounter Reason: 1\nByType 55607006:Problem: 1\nByStatus MissingStatus: 1\nByStatus active:active: 1\nBySourceCodingSystem 2.16.840.1.113883.6.103: 2" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#Total", + "valueInteger": 4 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#MissingDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#ByStatus", + "valueQuantity": { + "value": 4, + "unit": "completed:completed", + "code": "completed:completed" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#BySourceCodingSystem", + "valueQuantity": { + "value": 2, + "unit": "2.16.840.1.113883.6.12", + "code": "2.16.840.1.113883.6.12" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "2.16.840.1.113883.6.103", + "code": "2.16.840.1.113883.6.103" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Procedures#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "Proprietary.CareEvolution", + "code": "Proprietary.CareEvolution" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Procedures" + } + ], + "text": "Procedures:\nTotal: 4\nMissingDate: 0\nByStatus completed:completed: 4\nBySourceCodingSystem 2.16.840.1.113883.6.12: 2\nBySourceCodingSystem 2.16.840.1.113883.6.103: 1\nBySourceCodingSystem Proprietary.CareEvolution: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#Total", + "valueInteger": 5 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#MissingOrderDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#MissingStopDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#MissingOrderingCaregiver", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#MissingCaregiver", + "valueInteger": 5 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#ByStatus", + "valueQuantity": { + "value": 5, + "unit": "completed:completed", + "code": "completed:completed" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#BySourceCodingSystem", + "valueQuantity": { + "value": 4, + "unit": "2.16.840.1.113883.6.88", + "code": "2.16.840.1.113883.6.88" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Medications#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "Proprietary.CareEvolution", + "code": "Proprietary.CareEvolution" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Medications" + } + ], + "text": "Medications:\nTotal: 5\nMissingOrderDate: 0\nMissingStopDate: 0\nMissingOrderingCaregiver: 0\nMissingCaregiver: 5\nByStatus completed:completed: 5\nBySourceCodingSystem 2.16.840.1.113883.6.88: 4\nBySourceCodingSystem Proprietary.CareEvolution: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Allergies#Total", + "valueInteger": 1 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Allergies#MissingReportedDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Allergies#MissingOnsetDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Allergies#MissingReactions", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Allergies#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "2.16.840.1.113883.6.96", + "code": "2.16.840.1.113883.6.96" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Allergies" + } + ], + "text": "Allergies:\nTotal: 1\nMissingReportedDate: 0\nMissingOnsetDate: 0\nMissingReactions: 0\nBySourceCodingSystem 2.16.840.1.113883.6.96: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabObservations#Total", + "valueInteger": 7 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabObservations#MissingDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabObservations#MissingPerformer", + "valueInteger": 7 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabObservations#BySourceCodingSystem", + "valueQuantity": { + "value": 6, + "unit": "2.16.840.1.113883.6.96", + "code": "2.16.840.1.113883.6.96" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabObservations#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "Proprietary.DemoNamespace", + "code": "Proprietary.DemoNamespace" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "LabObservations" + } + ], + "text": "LabObservations:\nTotal: 7\nMissingDate: 0\nMissingPerformer: 7\nBySourceCodingSystem 2.16.840.1.113883.6.96: 6\nBySourceCodingSystem Proprietary.DemoNamespace: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabPanels#Total", + "valueInteger": 4 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabPanels#MissingDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabPanels#WithSingleLabObservation", + "valueInteger": 3 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabPanels#BySourceCodingSystem", + "valueQuantity": { + "value": 3, + "unit": "2.16.840.1.113883.6.96", + "code": "2.16.840.1.113883.6.96" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/LabPanels#BySourceCodingSystem", + "valueQuantity": { + "value": 1, + "unit": "Proprietary.DemoNamespace", + "code": "Proprietary.DemoNamespace" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "LabPanels" + } + ], + "text": "LabPanels:\nTotal: 4\nMissingDate: 0\nWithSingleLabObservation: 3\nBySourceCodingSystem 2.16.840.1.113883.6.96: 3\nBySourceCodingSystem Proprietary.DemoNamespace: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#Total", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#MissingAdmitDate", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#MissingPatientClass", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#MissingLocation", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#MissingCaregiversByPatientClass", + "valueQuantity": { + "value": 1, + "unit": "PCPVisit:PCP Visit", + "code": "PCPVisit:PCP Visit" + } + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/CDAParsingSummary/Encounters#CaregiverRelationshipsByType", + "valueQuantity": { + "value": 1, + "unit": "ScheduledCaregiver", + "code": "ScheduledCaregiver" + } + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/CDAParsingSummary", + "code": "Encounters" + } + ], + "text": "Encounters:\nTotal: 2\nMissingAdmitDate: 0\nMissingPatientClass: 0\nMissingLocation: 2\nMissingCaregiversByPatientClass PCPVisit:PCP Visit: 1\nCaregiverRelationshipsByType ScheduledCaregiver: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#Total", + "valueInteger": 5 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#WellCodedCount", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#EnhancedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#ParsedCount", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#GarbageCount", + "valueInteger": 1 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/RosettaCodingUplift", + "code": "Medications" + } + ], + "text": "Medications:\nTotal: 5\nWellCodedCount: 2\nEnhancedCount: 0\nParsedCount: 2\nGarbageCount: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#Total", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#WellCodedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#EnhancedCount", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#ParsedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#GarbageCount", + "valueInteger": 0 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/RosettaCodingUplift", + "code": "Problems" + } + ], + "text": "Problems:\nTotal: 2\nWellCodedCount: 0\nEnhancedCount: 2\nParsedCount: 0\nGarbageCount: 0" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#Total", + "valueInteger": 4 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#WellCodedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#EnhancedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#ParsedCount", + "valueInteger": 2 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#GarbageCount", + "valueInteger": 2 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/RosettaCodingUplift", + "code": "Services" + } + ], + "text": "Services:\nTotal: 4\nWellCodedCount: 0\nEnhancedCount: 0\nParsedCount: 2\nGarbageCount: 2" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#Total", + "valueInteger": 7 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#WellCodedCount", + "valueInteger": 6 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#EnhancedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#ParsedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#GarbageCount", + "valueInteger": 1 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/RosettaCodingUplift", + "code": "Labs" + } + ], + "text": "Labs:\nTotal: 7\nWellCodedCount: 6\nEnhancedCount: 0\nParsedCount: 0\nGarbageCount: 1" + } + }, + { + "severity": "information", + "code": "informational", + "details": { + "extension": [ + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#Total", + "valueInteger": 4 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#WellCodedCount", + "valueInteger": 3 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#EnhancedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#ParsedCount", + "valueInteger": 0 + }, + { + "url": "https://quality.rosetta.careevolution.com/v1/Extensions/RosettaUplift/Coding#GarbageCount", + "valueInteger": 1 + } + ], + "coding": [ + { + "system": "https://quality.rosetta.careevolution.com/v1/CodeSystems/RosettaCodingUplift", + "code": "Reports" + } + ], + "text": "Reports:\nTotal: 4\nWellCodedCount: 3\nEnhancedCount: 0\nParsedCount: 0\nGarbageCount: 1" + } + } + ] + }, + "search": { + "mode": "outcome" + } + } + ] +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/r4_bundle.json b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/r4_bundle.json new file mode 100644 index 0000000..337b840 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/r4_bundle.json @@ -0,0 +1,65 @@ +{ + "resourceType": "Bundle", + "type": "batch-response", + "entry": [ + { + "fullUrl": "https://api.careevolutionapi.com/Patient/35b77437-425d-419c-90b5-af4bc433ebe9", + "resource": { + "resourceType": "Patient", + "id": "35b77437-425d-419c-90b5-af4bc433ebe9", + "meta": { + "source": "http://rosetta.careevolution.com/identifiers/CareEvolution/MRN/1.3.6.1.4.1.37608_1.3.6.1.4.1.37608" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:1.3.6.1.4.1.37608", + "value": "IheTestPatient" + }, + { + "system": "http://rosetta.careevolution.com/identifiers/Proprietary/1.3.6.1.4.1.37608", + "value": "IheTestPatient" + } + ], + "name": [ + { + "use": "official", + "family": "Smith", + "given": [ + "Patient" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "534-555-6666", + "use": "home" + } + ], + "gender": "female", + "birthDate": "1956-08-13", + "deceasedBoolean": false, + "address": [ + { + "use": "home", + "line": [ + "34 Drury Lane" + ], + "city": "Disney Land", + "state": "CA", + "postalCode": "90210" + } + ] + } + } + ] +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/risk_profile_bundle.json b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/risk_profile_bundle.json new file mode 100644 index 0000000..ea71644 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/risk_profile_bundle.json @@ -0,0 +1,350 @@ +{ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "9cee689b-6501-4349-af32-e6849e179a2f", + "meta": { + "lastUpdated": "2023-02-22T11:27:29.9499804+00:00", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Patient" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-religion", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.1", + "code": "Example", + "display": "Example", + "userSelected": true + } + ] + } + }, + { + "extension": [ + { + "url": "text", + "valueString": "N" + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + }, + { + "extension": [ + { + "url": "text", + "valueString": "Black" + }, + { + "url": "detailed", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2056-0", + "display": "BLACK", + "userSelected": false + } + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + } + ], + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://test.careevolution.com/identifiers/CareEvolution/MRN/1.2.3.4.5.1.7_1.2.3.4.5.1.7", + "value": "0i56756845575l8yw6u886k4" + }, + { + "system": "http://test.careevolution.com/identifiers/1.2.3.4.5.1.7/1.2.3.4.5.1.7", + "value": "0i56756845575l8yw6u886k4" + }, + { + "system": "http://test.careevolution.com/identifiers/1.2.3.4.5.1.7/1.3.6.1.4.1.5641", + "value": "46274464" + } + ], + "name": [ + { + "use": "official", + "_use": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://careevolution.com/fhircodes#NameType", + "code": "LegalName", + "display": "Legal Name", + "userSelected": true + } + ] + } + } + ] + }, + "family": "Tester", + "given": [ + "Brittany" + ] + }, + { + "_use": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + }, + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://careevolution.com/fhircodes#NameType", + "code": "P", + "display": "Pseudonym", + "userSelected": true + } + ] + } + } + ] + }, + "family": "Tester", + "given": [ + "Brittany" + ] + } + ], + "telecom": [ + { + "system": "phone", + "_system": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://careevolution.com/fhircodes#ContactInfoType", + "code": "HomePhone", + "display": "Home Phone", + "userSelected": true + } + ] + } + } + ] + }, + "value": "tel:(680)555-1234", + "use": "home", + "_use": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.7", + "code": "HP", + "display": "primary home", + "userSelected": true + } + ] + } + } + ] + } + }, + { + "system": "phone", + "_system": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://careevolution.com/fhircodes#ContactInfoType", + "code": "OfficePhone", + "display": "Office Phone", + "userSelected": true + } + ] + } + } + ] + }, + "value": "tel:(548)555-8765", + "use": "work", + "_use": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.7", + "code": "WP", + "display": "work place", + "userSelected": true + } + ] + } + } + ] + } + }, + { + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + }, + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.7", + "code": "MC", + "display": "mobile contact", + "userSelected": true + } + ] + } + } + ] + }, + "value": "tel:(574)555-3737" + }, + { + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + }, + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.7", + "code": "Other", + "display": "Other", + "userSelected": true + } + ] + } + } + ] + }, + "value": "tel:(189)555-333" + } + ], + "gender": "male", + "_gender": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.5.1", + "code": "M", + "display": "Male", + "userSelected": true + }, + { + "system": "http://careevolution.com", + "code": "M", + "display": "Male", + "userSelected": false + }, + { + "system": "http://test.careevolution.com/codes/FhirCodesAlternate1/Gender", + "code": "M", + "display": "M", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/v3/AdministrativeGender", + "code": "M", + "display": "Male", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/administrative-gender", + "code": "male", + "display": "male", + "userSelected": false + }, + { + "system": "http://test.careevolution.com/codes/FhirCodes/Gender", + "code": "male", + "userSelected": false + } + ] + } + } + ] + }, + "birthDate": "1948-12-17", + "deceasedBoolean": false, + "address": [ + { + "use": "home", + "line": [ + "2608 Main Street" + ], + "city": "Anytown", + "state": "MI", + "postalCode": "48761" + } + ], + "maritalStatus": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.1", + "code": "M", + "display": "M", + "userSelected": true + } + ] + }, + "communication": [ + { + "language": { + "coding": [ + { + "system": "urn:oid:1.2.3.4.5.1.7", + "code": "ENGLISH", + "display": "ENGLISH", + "userSelected": true + } + ] + }, + "preferred": true + } + ] + } + } + ] +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/stu3_bundle.json b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/stu3_bundle.json new file mode 100644 index 0000000..9ea9611 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/stu3_bundle.json @@ -0,0 +1,2611 @@ +{ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "resource": { + "resourceType": "Patient", + "id": "3ead5e15-4da5-480b-8a94-ffea1e936809", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.723+00:00", + "security": [ + { + "system": "http://careevolution.com/accesspolicyname", + "code": "Standard Record Policy" + } + ] + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#patientInstitutions", + "valueCoding": { + "id": "patient-institution1", + "code": "BeaconInstitution", + "display": "BeaconInstitution" + } + } + ], + "identifier": [ + { + "id": "id1", + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest", + "value": "1234A" + }, + { + "id": "id2", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN", + "value": "1234A" + }, + { + "id": "id3", + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "name": [ + { + "id": "name1", + "use": "official", + "family": "Smith", + "given": [ + "John" + ] + } + ], + "gender": "male", + "_gender": { + "id": "gender1", + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "id": "gender2", + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/Gender", + "code": "M", + "display": "Male", + "userSelected": true + } + ] + } + } + ] + }, + "birthDate": "1961-01-02", + "_birthDate": { + "id": "birthdate1" + }, + "deceasedBoolean": false, + "_deceasedBoolean": { + "id": "deceased1" + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/0-3a4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "0-3a4295efae7fed11b9cc0e32e07a5c1b", + "contained": [ + { + "resourceType": "Patient", + "id": "patient", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "security": [ + { + "system": "http://careevolution.com/accesspolicyname", + "code": "Standard Record Policy" + } + ] + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#patientInstitutions", + "valueCoding": { + "id": "patient-institution1", + "code": "BeaconInstitution", + "display": "BeaconInstitution" + } + } + ], + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest", + "value": "1234A" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN", + "value": "1234A" + }, + { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "name": [ + { + "use": "official", + "_use": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/NameType", + "code": "LegalName", + "display": "Legal Name", + "userSelected": true + } + ] + } + } + ] + }, + "family": "Smith", + "given": [ + "John" + ] + } + ], + "gender": "male", + "_gender": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/Gender", + "code": "M", + "display": "Male", + "userSelected": true + } + ] + } + } + ] + }, + "birthDate": "1961-01-02", + "deceasedBoolean": false + } + ], + "target": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#id3" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#name1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#birthdate1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#deceased1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#gender1" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#gender2" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/targetElement", + "valueUri": "#patient-institution1" + } + ], + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809" + } + ], + "period": { + "start": "2022-12-19T15:08:05.723+00:00", + "end": "2022-12-19T15:08:05.717+00:00" + }, + "recorded": "2022-12-19T15:08:05.717+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ], + "entity": [ + { + "role": "source", + "whatReference": { + "reference": "#patient" + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Condition/5.d30a4c15f97fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.d30a4c15f97fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T23:58:55.387+00:00" + }, + "clinicalStatus": "resolved", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "resolved", + "userSelected": true + } + ] + } + } + ] + }, + "_verificationStatus": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unsupported" + }, + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "unconfirmed", + "userSelected": true + } + ] + } + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/condition-category", + "code": "encounter-diagnosis" + }, + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "EncounterDiagnosis", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/fhir-diagnosis-role/Reference", + "code": "CC", + "display": "Chief complaint", + "userSelected": false + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "flu", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "6142004", + "display": "Influenza (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "J11.1", + "display": "Influenza due to unidentified influenza virus with other respiratory manifestations", + "userSelected": false + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "onsetDateTime": "2022-12-19T02:31:55.382-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Condition/5.3d4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.3d4295efae7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.75+00:00" + }, + "clinicalStatus": "inactive", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "inactive", + "userSelected": true + } + ] + } + } + ] + }, + "verificationStatus": "confirmed", + "_verificationStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "confirmed", + "userSelected": true + } + ] + } + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/condition-category", + "code": "encounter-diagnosis" + }, + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "EncounterDiagnosis", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/fhir-diagnosis-role/Reference", + "code": "CC", + "display": "Chief complaint", + "userSelected": false + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "multiple sclerosis", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "24700007", + "display": "Multiple sclerosis (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "G35", + "display": "Multiple sclerosis", + "userSelected": false + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "onsetDateTime": "2022-12-18T18:55:05.675-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Condition/5.3c4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Condition", + "id": "5.3c4295efae7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T15:08:05.74+00:00" + }, + "clinicalStatus": "active", + "_clinicalStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisStatus", + "code": "Active", + "userSelected": true + } + ] + } + } + ] + }, + "verificationStatus": "confirmed", + "_verificationStatus": { + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#term", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisVerificationStatus", + "code": "confirmed", + "userSelected": true + } + ] + } + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisType", + "code": "SECONDARY", + "display": "SECONDARY", + "userSelected": true + }, + { + "system": "http://fhir.carevolution.com/codes/CareEvolution/DiagnosisType", + "code": "Secondary", + "display": "Secondary", + "userSelected": false + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/DiagnosisCode", + "code": "crohns disease", + "userSelected": true + }, + { + "system": "http://snomed.info/sct", + "code": "34000006", + "display": "Crohn's disease (disorder)", + "userSelected": false + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "K50.90", + "display": "Crohn's disease, unspecified, without complications", + "userSelected": false + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "onsetDateTime": "2022-12-18T17:20:05.675-05:00" + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/4-d30a4c15f97fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-d30a4c15f97fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/7.d30a4c15f97fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T23:58:55.387+00:00", + "end": "2022-12-19T23:58:55.387+00:00" + }, + "recorded": "2022-12-19T23:58:55.387+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/4-3d4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-3d4295efae7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/7.3d4295efae7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T15:08:05.75+00:00", + "end": "2022-12-19T15:08:05.75+00:00" + }, + "recorded": "2022-12-19T15:08:05.75+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/4-3c4295efae7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "4-3c4295efae7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Condition/7.3c4295efae7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T15:08:05.74+00:00", + "end": "2022-12-19T15:08:05.74+00:00" + }, + "recorded": "2022-12-19T15:08:05.74+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.682d4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.682d4f56bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:44:05.61+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T13:45:05.605-05:00", + "issued": "2022-12-18T13:45:05.605-05:00", + "valueQuantity": { + "value": 1690728474 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.d12b4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.d12b4f56bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:44:03.04+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T14:56:03.025-05:00", + "issued": "2022-12-18T14:56:03.025-05:00", + "valueQuantity": { + "value": 1271126762 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.028d6f3ebc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.028d6f3ebc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:43:26.953+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T17:09:26.943-05:00", + "issued": "2022-12-18T17:09:26.943-05:00", + "valueQuantity": { + "value": 295302631 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.54e37a32bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.54e37a32bc7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:43:10.48+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T10:31:10.464-05:00", + "issued": "2022-12-19T10:31:10.464-05:00", + "valueQuantity": { + "value": 652309659 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.d24b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.d24b67f6bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:41:23.93+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T11:44:23.919-05:00", + "issued": "2022-12-18T11:44:23.919-05:00", + "valueQuantity": { + "value": 789662810 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.a94b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.a94b67f6bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:41:22.403+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T11:56:22.403-05:00", + "issued": "2022-12-18T11:56:22.403-05:00", + "valueQuantity": { + "value": 1582118206 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.f1b562d2bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.f1b562d2bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:40:27.56+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-18T22:38:27.543-05:00", + "issued": "2022-12-18T22:38:27.543-05:00", + "valueQuantity": { + "value": 252600252 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.2d6720babb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.2d6720babb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-20T03:22:56.517+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T11:21:45.582-05:00", + "issued": "2022-12-19T11:21:45.582-05:00", + "valueQuantity": { + "value": 1724700070 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.b185f6a7bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.b185f6a7bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:39:16.903+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T10:56:16.9-05:00", + "issued": "2022-12-19T10:56:16.9-05:00", + "valueQuantity": { + "value": 2126651898 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:37:09.9+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T07:24:09.891-05:00", + "issued": "2022-12-19T07:24:09.891-05:00", + "valueQuantity": { + "value": 941137949 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.1a5cf010bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.1a5cf010bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:35:02.83+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T10:44:02.819-05:00", + "issued": "2022-12-19T10:44:02.819-05:00", + "valueQuantity": { + "value": 1868889329 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Observation/1.f59dca04bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Observation", + "id": "1.f59dca04bb7fed11b9cc0e32e07a5c1b", + "meta": { + "extension": [ + { + "url": "http://hl7.org/fhir/4.0/StructureDefinition/extension-meta.source", + "valueUri": "http://fhir.carevolution.com/identifiers/CareEvolution/MRN/problemSelectorTest" + } + ], + "lastUpdated": "2022-12-19T16:34:40.32+00:00" + }, + "extension": [ + { + "url": "http://careevolution.com/fhirextensions#observation-contextID", + "valueString": "f39dca04-bb7f-ed11-b9cc-0e32e07a5c1b" + } + ], + "status": "unknown", + "code": { + "coding": [ + { + "system": "http://fhir.carevolution.com/codes/DemoNamespace/ObservationType", + "code": "BeaconFakeObservation", + "userSelected": true + } + ] + }, + "subject": { + "reference": "Patient/3ead5e15-4da5-480b-8a94-ffea1e936809", + "identifier": { + "system": "http://careevolution.com/fhir/PatientId", + "value": "3a4295ef-ae7f-ed11-b9cc-0e32e07a5c1b" + } + }, + "effectiveDateTime": "2022-12-19T11:11:40.299-05:00", + "issued": "2022-12-19T11:11:40.299-05:00", + "valueQuantity": { + "value": 1577878080 + } + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-682d4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-682d4f56bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.682d4f56bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:44:05.61+00:00", + "end": "2022-12-19T16:44:05.61+00:00" + }, + "recorded": "2022-12-19T16:44:05.61+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-d12b4f56bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-d12b4f56bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.d12b4f56bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:44:03.04+00:00", + "end": "2022-12-19T16:44:03.04+00:00" + }, + "recorded": "2022-12-19T16:44:03.04+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-028d6f3ebc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-028d6f3ebc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.028d6f3ebc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:43:26.953+00:00", + "end": "2022-12-19T16:43:26.953+00:00" + }, + "recorded": "2022-12-19T16:43:26.953+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-54e37a32bc7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-54e37a32bc7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.54e37a32bc7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:43:10.48+00:00", + "end": "2022-12-19T16:43:10.48+00:00" + }, + "recorded": "2022-12-19T16:43:10.48+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-d24b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-d24b67f6bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.d24b67f6bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:41:23.93+00:00", + "end": "2022-12-19T16:41:23.93+00:00" + }, + "recorded": "2022-12-19T16:41:23.93+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-a94b67f6bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-a94b67f6bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.a94b67f6bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:41:22.403+00:00", + "end": "2022-12-19T16:41:22.403+00:00" + }, + "recorded": "2022-12-19T16:41:22.403+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-f1b562d2bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-f1b562d2bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.f1b562d2bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:40:27.56+00:00", + "end": "2022-12-19T16:40:27.56+00:00" + }, + "recorded": "2022-12-19T16:40:27.56+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-2d6720babb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-2d6720babb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.2d6720babb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:39:45.6+00:00", + "end": "2022-12-20T03:22:56.517+00:00" + }, + "recorded": "2022-12-20T03:22:56.517+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-b185f6a7bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-b185f6a7bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.b185f6a7bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:39:16.903+00:00", + "end": "2022-12-19T16:39:16.903+00:00" + }, + "recorded": "2022-12-19T16:39:16.903+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-74ec7d5fbb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.74ec7d5fbb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:37:09.9+00:00", + "end": "2022-12-19T16:37:09.9+00:00" + }, + "recorded": "2022-12-19T16:37:09.9+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-1a5cf010bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-1a5cf010bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.1a5cf010bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:35:02.83+00:00", + "end": "2022-12-19T16:35:02.83+00:00" + }, + "recorded": "2022-12-19T16:35:02.83+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + }, + { + "fullUrl": "https://fhir.careevolution.com/Master.Adapter1.WebClient/api/fhir-stu3/Provenance/16-f59dca04bb7fed11b9cc0e32e07a5c1b", + "resource": { + "resourceType": "Provenance", + "id": "16-f59dca04bb7fed11b9cc0e32e07a5c1b", + "target": [ + { + "reference": "Observation/1.f59dca04bb7fed11b9cc0e32e07a5c1b" + } + ], + "period": { + "start": "2022-12-19T16:34:40.32+00:00", + "end": "2022-12-19T16:34:40.32+00:00" + }, + "recorded": "2022-12-19T16:34:40.32+00:00", + "activity": { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE" + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/ParticipationType", + "code": "AUT", + "display": "Author (originator)" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "author" + } + ], + "text": "Originating organization" + } + ], + "whoReference": { + "reference": "Organization/65db5f61-777c-ed11-b9cc-0e32e07a5c1b", + "display": "problemSelectorTest" + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://dicom.nema.org/resources/ontology/DCM", + "code": "110150", + "display": "Application" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "performer" + } + ], + "text": "Application" + } + ], + "whoReference": { + "display": "Sweetriver", + "identifier": { + "use": "official", + "system": "http://fhir.carevolution.com/typeid/Application", + "value": "1" + } + } + }, + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser" + }, + { + "system": "http://hl7.org/fhir/provenance-participant-role", + "code": "enterer" + } + ], + "text": "User" + } + ], + "whoReference": { + "display": "SystemUser", + "identifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:3539c314-6e5b-4864-87c1-1195e7e2adcd" + } + } + } + ] + } + } + ] +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/x12.txt b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/x12.txt new file mode 100644 index 0000000..4b72ab9 --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveData/x12.txt @@ -0,0 +1,44 @@ +ISA*00* *00* *ZZ*SUBMITTERID *ZZ*RECEIVERID *230616*1145*^*00501*000000001*0*T*:~ +GS*HC*SENDERCODE*RECEIVERCODE*20230627*11301505*123456789*X*005010X222A1~ +ST*837*0034*005010X223A1~ +BHT*0019*00*3920394930203*20100816*1615*CH~ +NM1*41*2*HOWDEE HOSPITAL*****46*0123456789~ +PER*IC*BETTY RUBBLE*TE*9195551111~ +NM1*40*2*BCBSNC*****46*987654321~ +HL*1**20*1~ +NM1*85*2*HOWDEE HOSPITAL*****XX*1245011012~ +N3*123 HOWDEE BLVD~ +N4*DURHAM*NC*27701~ +REF*EI*123456789~ +PER*IC*WILMA RUBBLE*TE*9195551111*FX*6145551212~ +HL*2*1*22*0~ +SBR*P*18*XYZ1234567******BL~ +NM1*IL*1*DOUGH*MARY****MI*24672148306~ +N3*BOX 12312~ +N4*DURHAM*NC*27715~ +DMG*D8*19670807*F~ +NM1*PR*2*BCBSNC*****PI*987654321~ +CLM*2235057*200***13:A:1***A**Y*Y~ +DTP*434*RD8*20100730-20100730~ +CL1*1*9*01~ +REF*F8*ASD0000123~ +HI*BK:25000~ +HI*BF:78901~ +HI*BR:4491:D8:20100730~ +HI*BH:41:D8:20100501*BH:27:D8:20100715*BH:33:D8:20100415*BH:C2:D8:20100410~ +HI*BE:30:::20~ +HI*BG:01~ +NM1*71*1*SMITH*ELIZABETH*AL***34*243898989~ +REF*1G*P97777~ +LX*1~ +SV2*0300*HC:81000*120*UN*1~ +DTP*472*D8*20100730~ +LX*2~ +SV2*0320*HC:76092*50*UN*1~ +DTP*472*D8*20100730~ +LX*3~ +SV2*0270*HC:J1120*30*UN*1~ +DTP*472*D8*20100730~ +SE*38*0034~ +GE*1*30~ +IEA*1*000000031~ diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs new file mode 100644 index 0000000..ca9429d --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs @@ -0,0 +1,267 @@ +using CareEvolution.Orchestrate.Tests.Helpers; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class LiveIdentityApiTests +{ + private static readonly IdentityApi Api = LiveClients.CreateIdentityApi(); + private const string DefaultSource = "source"; + + private static readonly Demographic Demographic = new() + { + FirstName = "John", + LastName = "Doe", + Dob = "1980-01-01", + Gender = "male", + }; + + private static readonly BlindedDemographic BlindedDemographic = new() + { + Data = LiveTestData.BlindedDemographicData, + Version = 1, + }; + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task AddOrUpdateRecordShouldAddRecord() + { + var response = await Api.AddOrUpdateRecordAsync( + new AddOrUpdateRecordRequest + { + Source = DefaultSource, + Identifier = Guid.NewGuid().ToString(), + Demographic = Demographic, + } + ); + + Assert.NotNull(response.MatchedPerson?.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task AddOrUpdateRecordWithUrlUnsafeIdentifierShouldAddRecord() + { + var response = await Api.AddOrUpdateRecordAsync( + new AddOrUpdateRecordRequest + { + Source = DefaultSource, + Identifier = $"{Guid.NewGuid()}/", + Demographic = Demographic, + } + ); + + Assert.NotNull(response.MatchedPerson?.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task AddOrUpdateBlindedRecordShouldAddRecord() + { + var response = await Api.AddOrUpdateBlindedRecordAsync( + new AddOrUpdateBlindedRecordRequest + { + Source = DefaultSource, + Identifier = Guid.NewGuid().ToString(), + BlindedDemographic = BlindedDemographic, + } + ); + + Assert.NotNull(response.MatchedPerson?.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task AddOrUpdateBlindedRecordWithUrlUnsafeIdentifierShouldAddRecord() + { + var response = await Api.AddOrUpdateBlindedRecordAsync( + new AddOrUpdateBlindedRecordRequest + { + Source = DefaultSource, + Identifier = $"{Guid.NewGuid()}+/=", + BlindedDemographic = BlindedDemographic, + } + ); + + Assert.NotNull(response.MatchedPerson?.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task GetPersonByRecordShouldMatch() + { + var (person, identifier) = await CreateRandomRecordAsync(); + var response = await Api.GetPersonByRecordAsync( + new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = identifier, + } + ); + Assert.Equal(person.Id, response.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task GetPersonByIdShouldMatch() + { + var (person, _) = await CreateRandomRecordAsync(); + var response = await Api.GetPersonByIdAsync(new GetPersonByIdRequest { Id = person.Id }); + Assert.Equal(person.Id, response.Id); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task MatchDemographicsShouldMatch() + { + var response = await Api.MatchDemographicsAsync(Demographic); + Assert.NotNull(response); + Assert.NotNull(response.MatchingPersons); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task MatchBlindedDemographicsShouldMatch() + { + var response = await Api.MatchBlindedDemographicsAsync(BlindedDemographic); + Assert.NotNull(response); + Assert.NotNull(response.MatchingPersons); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task DeleteRecordShouldDelete() + { + var (person, identifier) = await CreateRandomRecordAsync(); + var response = await Api.DeleteRecordAsync( + new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = identifier, + } + ); + + Assert.Contains(response.ChangedPersons, changedPerson => changedPerson.Id == person.Id); + Assert.Contains( + response.ChangedPersons.SelectMany(changedPerson => changedPerson.Records), + record => record.Source == DefaultSource && record.Identifier == identifier + ); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task AddMatchGuidanceShouldReportChangedPersons() + { + var (firstPerson, firstIdentifier) = await CreateRandomRecordAsync(); + var (secondPerson, secondIdentifier) = await CreateRandomRecordAsync(); + + var response = await Api.AddMatchGuidanceAsync( + new AddMatchGuidanceRequest + { + RecordOne = new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = firstIdentifier, + }, + RecordTwo = new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = secondIdentifier, + }, + Action = "Match", + Comment = "Testing", + } + ); + + Assert.Contains( + response.ChangedPersons, + person => person.Id == firstPerson.Id || person.Id == secondPerson.Id + ); + } + + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] + public async Task RemoveMatchGuidanceShouldSeparatePersons() + { + var (firstPerson, firstIdentifier) = await CreateRandomRecordAsync(); + var (secondPerson, secondIdentifier) = await CreateRandomRecordAsync(); + var recordOne = new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = firstIdentifier, + }; + var recordTwo = new CareEvolution.Orchestrate.Identity.Record + { + Source = DefaultSource, + Identifier = secondIdentifier, + }; + + await Api.AddMatchGuidanceAsync( + new AddMatchGuidanceRequest + { + RecordOne = recordOne, + RecordTwo = recordTwo, + Action = "Match", + Comment = "Adding for removal testing", + } + ); + + var response = await Api.RemoveMatchGuidanceAsync( + new MatchGuidanceRequest + { + RecordOne = recordOne, + RecordTwo = recordTwo, + Comment = "Removal testing", + } + ); + + Assert.Contains( + response.ChangedPersons, + person => person.Id == firstPerson.Id || person.Id == secondPerson.Id + ); + } + + [LiveFact( + LiveTestEnvironment.IdentityApiKey, + LiveTestEnvironment.IdentityUrl, + LiveTestEnvironment.IdentityMetricsKey + )] + public async Task MonitoringIdentifierMetricsShouldHaveMetrics() + { + await CreateRandomRecordAsync(); + var response = await Api.Monitoring.IdentifierMetricsAsync(); + + Assert.False(string.IsNullOrWhiteSpace(response.Refreshed)); + Assert.True(response.TotalRecordCount > 0); + Assert.True(response.TotalPersonCount > 0); + Assert.NotNull(response.GlobalMetricsRecords); + Assert.Equal(string.Empty, response.GlobalMetricsRecords[0].Source); + Assert.Contains(response.SummaryMetricsRecords, record => record.Source == DefaultSource); + Assert.True(response.SourceTotals[0].TotalRecordCount > 0); + } + + [LiveFact( + LiveTestEnvironment.IdentityApiKey, + LiveTestEnvironment.IdentityUrl, + LiveTestEnvironment.IdentityMetricsKey + )] + public async Task MonitoringOverlapMetricsShouldHaveMetrics() + { + await CreateRandomRecordAsync(); + await CreateRandomRecordAsync(); + + var response = await Api.Monitoring.OverlapMetricsAsync(); + Assert.NotNull(response.DatasourceOverlapRecords); + Assert.Contains( + response.DatasourceOverlapRecords, + record => record.DatasourceA == DefaultSource + ); + Assert.Contains( + response.DatasourceOverlapRecords, + record => record.DatasourceB == DefaultSource + ); + Assert.True(response.DatasourceOverlapRecords[0].OverlapCount > 0); + } + + private static async Task<(Person Person, string Identifier)> CreateRandomRecordAsync() + { + var identifier = Guid.NewGuid().ToString(); + var response = await Api.AddOrUpdateRecordAsync( + new AddOrUpdateRecordRequest + { + Source = DefaultSource, + Identifier = identifier, + Demographic = Demographic, + } + ); + return (response.MatchedPerson!, identifier); + } +} diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs new file mode 100644 index 0000000..adf183b --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs @@ -0,0 +1,41 @@ +using CareEvolution.Orchestrate.Tests.Helpers; + +namespace CareEvolution.Orchestrate.Tests; + +public sealed class LiveLocalHashingApiTests +{ + private static readonly LocalHashingApi Api = LiveClients.CreateLocalHashingApi(); + + private static readonly Demographic Demographic = new() + { + FirstName = "John", + LastName = "Doe", + Dob = "1980-01-01", + Gender = "male", + }; + + [LiveFact(LiveTestEnvironment.LocalHashingUrl)] + public async Task HashShouldHashByDemographic() + { + var response = await Api.HashAsync(Demographic); + + Assert.True(response.Version > 0); + Assert.Equal([], response.Advisories?.InvalidDemographicFields ?? []); + } + + [LiveFact(LiveTestEnvironment.LocalHashingUrl)] + public async Task HashWithInvalidDemographicFieldsShouldReturnAdvisories() + { + var response = await Api.HashAsync( + new Demographic + { + FirstName = Demographic.FirstName, + LastName = Demographic.LastName, + Dob = "121980-01-01", + } + ); + + Assert.True(response.Version > 0); + Assert.Equal(["dob"], response.Advisories?.InvalidDemographicFields ?? []); + } +} From 8b7e96b609dcf96b1a8d89de9c45fe6c720a5915 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Mon, 30 Mar 2026 15:06:01 -0400 Subject: [PATCH 02/21] Fix Dotnet Setup Reference --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec8fb04..c20c498 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -178,7 +178,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup .NET - uses: actions/setup-dotnet@6d38f516158ab5bb06840831c7a2ec4f2dd7ddc1 # v5.0.0 + uses: actions/setup-dotnet@v5.2.0 # v5.2.0 with: dotnet-version: | 8.0.x From 3ad8a2841d046481bd5f32a1e7d1906997c302e8 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Mon, 30 Mar 2026 15:10:11 -0400 Subject: [PATCH 03/21] Publish to Proget --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c20c498..0b8cc44 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -206,7 +206,7 @@ jobs: working-directory: dotnet run: | dotnet nuget push ./artifacts/*.nupkg \ - --source https://api.nuget.org/v3/index.json \ - --api-key ${{ secrets.NUGET_API_KEY }} \ + --source https://proget.careevolution.com/nuget/nuget/ \ + --api-key ${{ secrets.PROGET_TOKEN }} \ --skip-duplicate echo "Published `${{ needs.version.outputs.version }}` to NuGet.org." >> $GITHUB_STEP_SUMMARY From 623890dc5646602b791b5ab59fd04d2c9468094d Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Mon, 30 Mar 2026 15:14:48 -0400 Subject: [PATCH 04/21] Fix Dotnet Setup Reference --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82135eb..914910f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -130,7 +130,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup .NET - uses: actions/setup-dotnet@6d38f516158ab5bb06840831c7a2ec4f2dd7ddc1 # v5.0.0 + uses: actions/setup-dotnet@v5.2.0 # v5.2.0 with: dotnet-version: ${{ matrix.dotnet-version }} - name: Restore dotnet tools From 7df3777163a300707117b0758b8f0f064f107c5c Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 15:49:02 -0400 Subject: [PATCH 05/21] Potential fix for code scanning alert no. 9: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 914910f..23fc316 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ name: Test +permissions: + contents: read + on: push: branches: From 3ba511e08657ca9ec0d303962381d6afc276985e Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 15:52:03 -0400 Subject: [PATCH 06/21] Remove Premaure Using See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3011619178 --- dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs b/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs index 98171c0..5b6b484 100644 --- a/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs +++ b/dotnet/src/CareEvolution.Orchestrate/ConvertApi.cs @@ -328,7 +328,7 @@ private Task PostTextAsync( CancellationToken cancellationToken ) { - using var httpContent = new StringContent(content); + var httpContent = new StringContent(content); httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue( contentType ); From 2ff4a2439a3d216c78a4c52bbb6080a0a14e31db Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 15:53:38 -0400 Subject: [PATCH 07/21] Address Different Casing in Authorization Scheme See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3011619201 --- .../EnvironmentConfiguration.cs | 18 +++++++++++++++++- .../ConfigurationTests.cs | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs index c768862..af86726 100644 --- a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -44,7 +44,7 @@ public static ResolvedConfiguration Resolve(IdentityApiOptions? options) ApiKey: GetPriority(options?.ApiKey, IdentityApiKeyEnvironmentVariable), Authorization: string.IsNullOrWhiteSpace(metricsKey) ? null - : $"Basic {metricsKey.Replace("Basic ", string.Empty, StringComparison.Ordinal)}" + : $"Basic {NormalizeBasicCredential(metricsKey)}" ); } @@ -118,4 +118,20 @@ private static IReadOnlyDictionary GetAdditionalHeaders() return headers; } + + private static string NormalizeBasicCredential(string metricsKey) + { + var normalized = metricsKey.Trim(); + const string prefix = "Basic"; + if ( + normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + && normalized.Length > prefix.Length + && char.IsWhiteSpace(normalized[prefix.Length]) + ) + { + normalized = normalized[(prefix.Length + 1)..].TrimStart(); + } + + return normalized; + } } diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index d3d1272..590f049 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -73,6 +73,8 @@ public void IdentityApiShouldRequireUrl() [Theory] [InlineData("metrics-key", "Basic metrics-key")] [InlineData("Basic metrics-key", "Basic metrics-key")] + [InlineData("basic metrics-key", "Basic metrics-key")] + [InlineData(" Basic metrics-key ", "Basic metrics-key")] public async Task IdentityApiShouldNormalizeMetricsKey( string rawMetricsKey, string expectedAuthorization From 853a29a29c64f19d1896db2cf2da0b911e04ee5e Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 15:54:50 -0400 Subject: [PATCH 08/21] Use RouteBuilder to Escape TerminologyApi URI See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3011619215 --- .../TerminologyApi.cs | 2 +- .../ApiSurfaceTests.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs b/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs index 0c1888c..93e12a0 100644 --- a/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs +++ b/dotnet/src/CareEvolution.Orchestrate/TerminologyApi.cs @@ -320,7 +320,7 @@ public Task GetFhirR4CodeSystemAsync( ) { var route = RouteBuilder.Build( - $"/terminology/v1/fhir/r4/codesystem/{request.CodeSystem}", + $"/terminology/v1/fhir/r4/codesystem/{RouteBuilder.Escape(request.CodeSystem)}", [ new KeyValuePair("page.num", request.PageNumber?.ToString()), new KeyValuePair("_count", request.PageSize?.ToString()), diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs index d97f087..f6dbca1 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -194,6 +194,31 @@ await api.Insight.RiskProfileAsync( ); } + [Fact] + public async Task GetFhirR4CodeSystemShouldEscapePathSegment() + { + var handler = new FakeHttpMessageHandler( + (_, _) => + Task.FromResult( + FakeResponses.Json("""{"resourceType":"CodeSystem","concept":[]}""") + ) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + _ = await api.Terminology.GetFhirR4CodeSystemAsync( + new GetFhirR4CodeSystemRequest { CodeSystem = "SNOMED/CT Demo" } + ); + + Assert.Equal( + "https://api.example.com/terminology/v1/fhir/r4/codesystem/SNOMED%2FCT%20Demo", + handler.LastRequest!.RequestUri!.AbsoluteUri + ); + } + [Fact] public void CombinedFhirBundleFactoryShouldGenerateNdjson() { From 61e97e0723c6cdb807ef9270f66ca244bbdfb26e Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:03:05 -0400 Subject: [PATCH 09/21] Make BaseUrl Failures More Clear See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3011619231 --- .../EnvironmentConfiguration.cs | 14 +++++++++- .../ConfigurationTests.cs | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs index af86726..a3eabff 100644 --- a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -19,7 +19,8 @@ internal static class EnvironmentConfiguration public static ResolvedConfiguration Resolve(OrchestrateClientOptions? options) { return new ResolvedConfiguration( - BaseUrl: GetPriority(options?.BaseUrl, BaseUrlEnvironmentVariable) ?? DefaultBaseUrl, + BaseUrl: GetPriorityOrMissing(options?.BaseUrl, BaseUrlEnvironmentVariable) + ?? DefaultBaseUrl, TimeoutMs: GetTimeout(options?.TimeoutMs), AdditionalHeaders: GetAdditionalHeaders(), ApiKey: GetPriority(options?.ApiKey, ApiKeyEnvironmentVariable) @@ -70,6 +71,17 @@ public static ResolvedConfiguration Resolve(LocalHashingApiOptions? options) return explicitValue ?? Environment.GetEnvironmentVariable(environmentVariable); } + private static string? GetPriorityOrMissing(string? explicitValue, string environmentVariable) + { + if (!string.IsNullOrWhiteSpace(explicitValue)) + { + return explicitValue; + } + + var environmentValue = Environment.GetEnvironmentVariable(environmentVariable); + return string.IsNullOrWhiteSpace(environmentValue) ? null : environmentValue; + } + private static int GetTimeout(int? explicitTimeoutMs) { var rawValue = diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 590f049..5e0300a 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -48,6 +48,32 @@ await api.Terminology.StandardizeConditionAsync( Assert.Equal("application/json", handler.LastRequest.Headers["Accept"].Single()); } + [Fact] + public async Task OrchestrateApiShouldTreatWhitespaceBaseUrlAsMissing() + { + using var environment = new EnvironmentVariableScope( + new Dictionary { ["ORCHESTRATE_BASE_URL"] = "https://env.example.com" } + ); + + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) + ); + using var httpClient = new HttpClient(handler); + using var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = " " } + ); + + await api.Terminology.StandardizeConditionAsync( + new StandardizeRequest { Code = "123", System = "SNOMED" } + ); + + Assert.Equal( + "https://env.example.com/terminology/v1/standardize/condition", + handler.LastRequest!.RequestUri!.ToString() + ); + } + [Fact] public void OrchestrateApiShouldThrowForInvalidTimeoutEnvironmentVariable() { From 6afbb6e6fe7066e553096fe27cbafa64e38b7709 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:03:57 -0400 Subject: [PATCH 10/21] Fix README See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3011619247 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 376c652..e77a31f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Additionally, C# also supports dependency injection with `IOrchestrateApi` and ` ```csharp using CareEvolution.Orchestrate; +using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); services.AddOrchestrateApi(); @@ -142,6 +143,7 @@ api = OrchestrateApi() With environment values as above or DI configuration: ```csharp +using Microsoft.Extensions.DependencyInjection; using CareEvolution.Orchestrate; var services = new ServiceCollection(); From 6f8cdff09776e9702d92a708c413f6e9630c74f7 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:14:24 -0400 Subject: [PATCH 11/21] Provide Exception Docs, Rename See - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012314269 - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012187372 --- ...eClientError.cs => OrchestrateClientException.cs} | 7 +++++-- .../Exceptions/OrchestrateError.cs | 3 --- .../Exceptions/OrchestrateException.cs | 6 ++++++ .../Exceptions/OrchestrateHttpError.cs | 9 --------- .../Exceptions/OrchestrateHttpException.cs | 12 ++++++++++++ .../OrchestrateHttpClient.cs | 4 ++-- .../ConfigurationTests.cs | 4 ++-- .../CareEvolution.Orchestrate.Tests/LiveApiTests.cs | 2 +- 8 files changed, 28 insertions(+), 19 deletions(-) rename dotnet/src/CareEvolution.Orchestrate/Exceptions/{OrchestrateClientError.cs => OrchestrateClientException.cs} (68%) delete mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateException.cs delete mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs create mode 100644 dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpException.cs diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs similarity index 68% rename from dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs rename to dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs index 32ed09b..aae19f5 100644 --- a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientError.cs +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs @@ -2,12 +2,15 @@ namespace CareEvolution.Orchestrate.Exceptions; -public sealed class OrchestrateClientError( +/// +/// Raised when the Orchestrate API returns a 4xx or 5xx status code. +/// +public sealed class OrchestrateClientException( string responseText, IReadOnlyList issues, HttpStatusCode statusCode ) - : OrchestrateHttpError( + : OrchestrateHttpException( issues.Count > 0 ? $"\n * {string.Join(" \n * ", issues)}" : responseText, statusCode ) diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs deleted file mode 100644 index 4805b56..0000000 --- a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateError.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace CareEvolution.Orchestrate.Exceptions; - -public class OrchestrateError(string message) : Exception(message) { } diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateException.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateException.cs new file mode 100644 index 0000000..f1c00ff --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateException.cs @@ -0,0 +1,6 @@ +namespace CareEvolution.Orchestrate.Exceptions; + +/// +/// Base class for all Orchestrate exceptions. +/// +public class OrchestrateException(string message) : Exception(message) { } diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs deleted file mode 100644 index 716cd55..0000000 --- a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpError.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Net; - -namespace CareEvolution.Orchestrate.Exceptions; - -public class OrchestrateHttpError(string message, HttpStatusCode? statusCode = null) - : OrchestrateError(message) -{ - public HttpStatusCode? StatusCode { get; } = statusCode; -} diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpException.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpException.cs new file mode 100644 index 0000000..ecf2c71 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateHttpException.cs @@ -0,0 +1,12 @@ +using System.Net; + +namespace CareEvolution.Orchestrate.Exceptions; + +/// +/// Raised when an HTTP request to the Orchestrate API fails. +/// +public class OrchestrateHttpException(string message, HttpStatusCode? statusCode = null) + : OrchestrateException(message) +{ + public HttpStatusCode? StatusCode { get; } = statusCode; +} diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs index 1cd85bc..9c8da57 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -343,10 +343,10 @@ CancellationToken cancellationToken var issues = ReadOperationalOutcomes(responseText); if ((int)response.StatusCode >= 400 && (int)response.StatusCode < 600) { - return new OrchestrateClientError(responseText, issues, response.StatusCode); + return new OrchestrateClientException(responseText, issues, response.StatusCode); } - return new OrchestrateHttpError(responseText, response.StatusCode); + return new OrchestrateHttpException(responseText, response.StatusCode); } private static IReadOnlyList ReadOperationalOutcomes(string responseText) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 5e0300a..1fb8054 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -129,7 +129,7 @@ string expectedAuthorization } [Fact] - public async Task HttpErrorsShouldBeConvertedToOrchestrateClientErrors() + public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() { var handler = new FakeHttpMessageHandler( (_, _) => @@ -147,7 +147,7 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientErrors() new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); - var exception = await Assert.ThrowsAsync(() => + var exception = await Assert.ThrowsAsync(() => api.Terminology.StandardizeConditionAsync( new StandardizeRequest { Code = "123", System = "SNOMED" } ) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs index d1c3c3e..7fbfbcf 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs @@ -843,7 +843,7 @@ public async Task GetFhirR4ValueSetScopesShouldReturnValueSet() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetsByScopeWithoutPaginationShouldRaise() { - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => Api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { Scope = "http://loinc.org" } ) From 9051b7668d3df5393df0d4d97050f7e7664e9d45 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:16:05 -0400 Subject: [PATCH 12/21] Add RouteBuilder Unit Tests See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3015827963 --- .../CareEvolution.Orchestrate/AssemblyInfo.cs | 3 + .../RouteBuilderTests.cs | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 dotnet/src/CareEvolution.Orchestrate/AssemblyInfo.cs create mode 100644 dotnet/tests/CareEvolution.Orchestrate.Tests/RouteBuilderTests.cs diff --git a/dotnet/src/CareEvolution.Orchestrate/AssemblyInfo.cs b/dotnet/src/CareEvolution.Orchestrate/AssemblyInfo.cs new file mode 100644 index 0000000..ca57be2 --- /dev/null +++ b/dotnet/src/CareEvolution.Orchestrate/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CareEvolution.Orchestrate.Tests")] diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/RouteBuilderTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/RouteBuilderTests.cs new file mode 100644 index 0000000..318efcb --- /dev/null +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/RouteBuilderTests.cs @@ -0,0 +1,65 @@ +namespace CareEvolution.Orchestrate.Tests; + +public sealed class RouteBuilderTests +{ + [Fact] + public void BuildShouldReturnPathWhenQueryIsEmpty() + { + var route = RouteBuilder.Build( + "/terminology/v1/fhir/r4/valueset", + [new KeyValuePair("name", null)] + ); + + Assert.Equal("/terminology/v1/fhir/r4/valueset", route); + } + + [Fact] + public void BuildShouldCombineMultipleQuerySets() + { + var route = RouteBuilder.Build( + "/convert/v1/hl7tofhirr4", + [new KeyValuePair("patientId", "1234")], + [ + new KeyValuePair("tz", "America/New_York"), + new KeyValuePair("processingHint", "lab"), + ] + ); + + Assert.Equal( + "/convert/v1/hl7tofhirr4?patientId=1234&tz=America%2FNew_York&processingHint=lab", + route + ); + } + + [Fact] + public void BuildQueryShouldSkipNullOrWhitespaceValues() + { + var query = RouteBuilder.BuildQuery([ + new KeyValuePair("name", "SNOMED"), + new KeyValuePair("blank", ""), + new KeyValuePair("spaces", " "), + new KeyValuePair("_count", "2"), + ]); + + Assert.Equal("name=SNOMED&_count=2", query); + } + + [Fact] + public void BuildQueryShouldEscapeKeysAndValues() + { + var query = RouteBuilder.BuildQuery([ + new KeyValuePair("concept:contains", "heart failure/test"), + new KeyValuePair("name", "SNOMED CT"), + ]); + + Assert.Equal("concept%3Acontains=heart%20failure%2Ftest&name=SNOMED%20CT", query); + } + + [Fact] + public void EscapeShouldEscapeReservedCharacters() + { + var escaped = RouteBuilder.Escape("SNOMED/CT Demo"); + + Assert.Equal("SNOMED%2FCT%20Demo", escaped); + } +} From 950a92abba881d365a6b8c64e1030528811537ec Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:38:27 -0400 Subject: [PATCH 13/21] Force Users to Come With HttpClient See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012299113 --- .../Identity/IdentityApi.cs | 9 +- .../Identity/LocalHashingApi.cs | 17 +- .../OrchestrateApi.cs | 9 +- .../OrchestrateHttpClient.cs | 10 +- .../ApiSurfaceTests.cs | 24 +-- .../ConfigurationTests.cs | 18 +- .../Helpers/LiveClients.cs | 6 +- .../IdentityApiTests.cs | 10 +- .../LiveApiTests.cs | 162 ++++++++++-------- .../LiveIdentityApiTests.cs | 47 +++-- .../LiveLocalHashingApiTests.cs | 19 +- 11 files changed, 175 insertions(+), 156 deletions(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs index f3b3e6a..4a16bed 100644 --- a/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/IdentityApi.cs @@ -2,14 +2,11 @@ namespace CareEvolution.Orchestrate.Identity; -public sealed class IdentityApi : IDisposable +public sealed class IdentityApi { private readonly OrchestrateHttpClient _http; - public IdentityApi(IdentityApiOptions? options = null) - : this(httpClient: null, options) { } - - public IdentityApi(HttpClient? httpClient, IdentityApiOptions? options = null) + public IdentityApi(HttpClient httpClient, IdentityApiOptions? options = null) { _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); Monitoring = new IdentityMonitoringApi(_http); @@ -106,8 +103,6 @@ public Task RemoveMatchGuidanceAsync( cancellationToken ); - public void Dispose() => _http.Dispose(); - private static string BuildSourceIdentifierRoute(string source, string identifier) => $"{RouteBuilder.Escape(source)}/{RouteBuilder.Escape(identifier)}"; } diff --git a/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs b/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs index 78201b9..ea22b1d 100644 --- a/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs +++ b/dotnet/src/CareEvolution.Orchestrate/Identity/LocalHashingApi.cs @@ -2,17 +2,12 @@ namespace CareEvolution.Orchestrate.Identity; -public sealed class LocalHashingApi : IDisposable +public sealed class LocalHashingApi(HttpClient httpClient, LocalHashingApiOptions? options = null) { - private readonly OrchestrateHttpClient _http; - - public LocalHashingApi(LocalHashingApiOptions? options = null) - : this(httpClient: null, options) { } - - public LocalHashingApi(HttpClient? httpClient, LocalHashingApiOptions? options = null) - { - _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); - } + private readonly OrchestrateHttpClient _http = new( + EnvironmentConfiguration.Resolve(options), + httpClient + ); [EditorBrowsable(EditorBrowsableState.Advanced)] public IOrchestrateHttpClient Transport => _http; @@ -23,6 +18,4 @@ public Task HashAsync( Demographic demographic, CancellationToken cancellationToken = default ) => _http.PostJsonAsync("/hash", demographic, cancellationToken); - - public void Dispose() => _http.Dispose(); } diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs index 6ba0cc3..a34c2b4 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateApi.cs @@ -2,7 +2,7 @@ namespace CareEvolution.Orchestrate; -public interface IOrchestrateApi : IDisposable +public interface IOrchestrateApi { ITerminologyApi Terminology { get; } IConvertApi Convert { get; } @@ -14,10 +14,7 @@ public sealed class OrchestrateApi : IOrchestrateApi { private readonly OrchestrateHttpClient _http; - public OrchestrateApi(OrchestrateClientOptions? options = null) - : this(httpClient: null, options) { } - - public OrchestrateApi(HttpClient? httpClient, OrchestrateClientOptions? options = null) + public OrchestrateApi(HttpClient httpClient, OrchestrateClientOptions? options = null) { _http = new OrchestrateHttpClient(EnvironmentConfiguration.Resolve(options), httpClient); Terminology = new TerminologyApi(_http); @@ -33,6 +30,4 @@ public OrchestrateApi(HttpClient? httpClient, OrchestrateClientOptions? options [EditorBrowsable(EditorBrowsableState.Advanced)] public IOrchestrateHttpClient HttpHandler => _http; - - public void Dispose() => _http.Dispose(); } diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs index 9c8da57..ded7b3c 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -12,7 +12,7 @@ namespace CareEvolution.Orchestrate; internal sealed class OrchestrateHttpClient( ResolvedConfiguration configuration, - HttpClient? httpClient = null + HttpClient httpClient ) : IOrchestrateHttpClient { private static readonly FhirJsonFastParser FhirJsonParser = CreateFhirJsonParser(); @@ -24,8 +24,7 @@ internal sealed class OrchestrateHttpClient( Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; - private readonly HttpClient _httpClient = httpClient ?? new HttpClient(); - private readonly bool _disposeHttpClient = httpClient is null; + private readonly HttpClient _httpClient = httpClient; private readonly ResolvedConfiguration _configuration = configuration; public HttpClient HttpClient => _httpClient; @@ -444,9 +443,6 @@ private static string GetIssueDetailString(JsonNode? detailNode) public void Dispose() { - if (_disposeHttpClient) - { - _httpClient.Dispose(); - } + _httpClient.Dispose(); } } diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs index f6dbca1..1c5d6bf 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -14,7 +14,7 @@ public async Task TerminologyBatchShouldPostToBatchRoute() (_, _) => Task.FromResult(FakeResponses.Json("""{"items":[{"coding":[]}]}""")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -39,7 +39,7 @@ public async Task ConvertHl7ShouldSendPlainTextAndQueryParameters() (_, _) => Task.FromResult(FakeResponses.Json("{}")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -72,7 +72,7 @@ public async Task AdvancedTransportShouldApplyBaseUrlAndDeserializeJson() (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -96,7 +96,7 @@ public async Task AdvancedTransportShouldSupportTextResponses() (_, _) => Task.FromResult(FakeResponses.Text("", "text/html")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -126,7 +126,7 @@ public void AddOrchestrateApiShouldRegisterIOrchestrateApi() }); using var serviceProvider = services.BuildServiceProvider(); - using var api = serviceProvider.GetRequiredService(); + var api = serviceProvider.GetRequiredService(); var options = serviceProvider .GetRequiredService>() .Value; @@ -149,7 +149,7 @@ public async Task ConvertPdfShouldReturnBytes() (_, _) => Task.FromResult(FakeResponses.Bytes(expected, "application/pdf")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -169,7 +169,7 @@ public async Task InsightRiskProfileShouldBuildExpectedQueryString() (_, _) => Task.FromResult(FakeResponses.Json("{}")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -204,7 +204,7 @@ public async Task GetFhirR4CodeSystemShouldEscapePathSegment() ) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -248,7 +248,7 @@ public async Task StandardizeBundleShouldSerializeAsFhirJson() ) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -275,7 +275,7 @@ public async Task GetAllFhirR4ValueSetsForCodesShouldSerializeParametersWithValu ) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -317,7 +317,7 @@ public async Task ConvertFhirR4ToOmopShouldSerializeAsFhirJson() (_, _) => Task.FromResult(FakeResponses.Bytes([80, 75, 3, 4], "application/zip")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); @@ -343,7 +343,7 @@ public async Task ConvertFhirR4ToNemsisV35ShouldSerializeAsFhirJson() (_, _) => Task.FromResult(FakeResponses.Text("", "application/xml")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 1fb8054..369890a 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -24,7 +24,7 @@ public async Task OrchestrateApiShouldPreferConstructorValuesOverEnvironmentVari (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { @@ -59,7 +59,7 @@ public async Task OrchestrateApiShouldTreatWhitespaceBaseUrlAsMissing() (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = " " } ); @@ -81,7 +81,10 @@ public void OrchestrateApiShouldThrowForInvalidTimeoutEnvironmentVariable() new Dictionary { ["ORCHESTRATE_TIMEOUT_MS"] = "not-a-number" } ); - var exception = Assert.Throws(() => new OrchestrateApi()); + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new OrchestrateApi(httpClient)); Assert.Contains("ORCHESTRATE_TIMEOUT_MS", exception.Message); } @@ -92,7 +95,10 @@ public void IdentityApiShouldRequireUrl() new Dictionary { ["ORCHESTRATE_IDENTITY_URL"] = null } ); - var exception = Assert.Throws(() => new IdentityApi()); + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new IdentityApi(httpClient)); Assert.Contains("Identity URL is required", exception.Message); } @@ -117,7 +123,7 @@ string expectedAuthorization (_, _) => Task.FromResult(FakeResponses.Json("""{"datasourceOverlapRecords":[]}""")) ); using var httpClient = new HttpClient(handler); - using var api = new IdentityApi( + var api = new IdentityApi( httpClient, new IdentityApiOptions { MetricsKey = rawMetricsKey } ); @@ -142,7 +148,7 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() ); using var httpClient = new HttpClient(handler); - using var api = new OrchestrateApi( + var api = new OrchestrateApi( httpClient, new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } ); diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs index ae24fb5..e816bf8 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/Helpers/LiveClients.cs @@ -2,9 +2,9 @@ namespace CareEvolution.Orchestrate.Tests.Helpers; internal static class LiveClients { - public static OrchestrateApi CreateOrchestrateApi() => new(); + public static OrchestrateApi CreateOrchestrateApi(HttpClient httpClient) => new(httpClient); - public static IdentityApi CreateIdentityApi() => new(); + public static IdentityApi CreateIdentityApi(HttpClient httpClient) => new(httpClient); - public static LocalHashingApi CreateLocalHashingApi() => new(); + public static LocalHashingApi CreateLocalHashingApi(HttpClient httpClient) => new(httpClient); } diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs index f8c41a0..8af6017 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs @@ -16,7 +16,7 @@ public async Task AddOrUpdateRecordShouldEncodeRouteAndSendDemographicPayload() ) ); using var httpClient = new HttpClient(handler); - using var api = new IdentityApi( + var api = new IdentityApi( httpClient, new IdentityApiOptions { Url = "https://identity.example.com" } ); @@ -55,7 +55,7 @@ public async Task AddOrUpdateBlindedRecordShouldFlattenPayload() ) ); using var httpClient = new HttpClient(handler); - using var api = new IdentityApi( + var api = new IdentityApi( httpClient, new IdentityApiOptions { Url = "https://identity.example.com" } ); @@ -84,7 +84,7 @@ public async Task DeleteRecordShouldSendEmptyObjectPayload() (_, _) => Task.FromResult(FakeResponses.Json("""{"changedPersons":[]}""")) ); using var httpClient = new HttpClient(handler); - using var api = new IdentityApi( + var api = new IdentityApi( httpClient, new IdentityApiOptions { Url = "https://identity.example.com" } ); @@ -112,7 +112,7 @@ public async Task MonitoringShouldCallExpectedRoute() ) ); using var httpClient = new HttpClient(handler); - using var api = new IdentityApi( + var api = new IdentityApi( httpClient, new IdentityApiOptions { Url = "https://identity.example.com" } ); @@ -145,7 +145,7 @@ public async Task LocalHashingShouldUseConfiguredUrl() ) ); using var httpClient = new HttpClient(handler); - using var api = new LocalHashingApi(httpClient); + var api = new LocalHashingApi(httpClient); var response = await api.HashAsync(new Demographic { FirstName = "John" }); diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs index 7fbfbcf..7f9562f 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs @@ -4,11 +4,12 @@ namespace CareEvolution.Orchestrate.Tests; -public sealed class LiveApiTests +public sealed class LiveApiTests : IDisposable { private static readonly byte[] PdfMagicNumber = [37, 80, 68, 70]; private static readonly byte[] PkZipMagicNumber = [80, 75, 3, 4]; - private static readonly OrchestrateApi Api = LiveClients.CreateOrchestrateApi(); + private readonly HttpClient _httpClient = new(); + private readonly OrchestrateApi _api; private static readonly ClassifyConditionRequest[] ClassifyConditionRequestItems = [ new ClassifyConditionRequest @@ -145,11 +146,16 @@ string ExpectedCode { StandardizeRadiologyCases[1].Request, StandardizeRadiologyCases[1].ExpectedCode }, }; + public LiveApiTests() + { + _api = LiveClients.CreateOrchestrateApi(_httpClient); + } + [LiveTheory(LiveTestEnvironment.OrchestrateApiKey)] [MemberData(nameof(ClassifyConditionRequests))] public async Task ClassifyConditionShouldClassifySingleRequest(ClassifyConditionRequest request) { - var result = await Api.Terminology.ClassifyConditionAsync(request); + var result = await _api.Terminology.ClassifyConditionAsync(request); Assert.NotNull(result); Assert.True(result.CciAcute); } @@ -157,7 +163,7 @@ public async Task ClassifyConditionShouldClassifySingleRequest(ClassifyCondition [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ClassifyConditionShouldClassifyBatch() { - var results = await Api.Terminology.ClassifyConditionAsync(ClassifyConditionRequestItems); + var results = await _api.Terminology.ClassifyConditionAsync(ClassifyConditionRequestItems); Assert.Equal(2, results.Count); Assert.All(results, result => Assert.True(result.CciAcute)); } @@ -168,14 +174,14 @@ public async Task ClassifyMedicationShouldClassifySingleRequest( ClassifyMedicationRequest request ) { - var result = await Api.Terminology.ClassifyMedicationAsync(request); + var result = await _api.Terminology.ClassifyMedicationAsync(request); Assert.True(result.RxNormGeneric); } [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ClassifyMedicationShouldClassifyBatch() { - var results = await Api.Terminology.ClassifyMedicationAsync(ClassifyMedicationRequestItems); + var results = await _api.Terminology.ClassifyMedicationAsync(ClassifyMedicationRequestItems); Assert.Equal(2, results.Count); Assert.All(results, result => Assert.True(result.RxNormGeneric)); } @@ -186,14 +192,14 @@ public async Task ClassifyObservationShouldClassifySingleRequest( ClassifyObservationRequest request ) { - var result = await Api.Terminology.ClassifyObservationAsync(request); + var result = await _api.Terminology.ClassifyObservationAsync(request); Assert.Equal("MICRO", result.LoincClass); } [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ClassifyObservationShouldClassifyBatch() { - var results = await Api.Terminology.ClassifyObservationAsync( + var results = await _api.Terminology.ClassifyObservationAsync( ClassifyObservationRequestItems ); Assert.Equal(2, results.Count); @@ -207,7 +213,7 @@ public async Task StandardizeConditionShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeConditionAsync(request); + var result = await _api.Terminology.StandardizeConditionAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -216,7 +222,7 @@ public async Task StandardizeConditionShouldStandardizeBatch() { var requests = StandardizeConditionCases.Select(row => row.Request).ToList(); var expected = StandardizeConditionCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeConditionAsync(requests); + var results = await _api.Terminology.StandardizeConditionAsync(requests); Assert.Equal(3, results.Count); for (var index = 0; index < results.Count; index++) { @@ -231,7 +237,7 @@ public async Task StandardizeLabShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeLabAsync(request); + var result = await _api.Terminology.StandardizeLabAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -240,7 +246,7 @@ public async Task StandardizeLabShouldStandardizeBatch() { var requests = StandardizeLabCases.Select(row => row.Request).ToList(); var expected = StandardizeLabCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeLabAsync(requests); + var results = await _api.Terminology.StandardizeLabAsync(requests); Assert.Equal(2, results.Count); for (var index = 0; index < results.Count; index++) { @@ -255,7 +261,7 @@ public async Task StandardizeMedicationShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeMedicationAsync(request); + var result = await _api.Terminology.StandardizeMedicationAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -264,7 +270,7 @@ public async Task StandardizeMedicationShouldStandardizeBatch() { var requests = StandardizeMedicationCases.Select(row => row.Request).ToList(); var expected = StandardizeMedicationCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeMedicationAsync(requests); + var results = await _api.Terminology.StandardizeMedicationAsync(requests); Assert.Equal(3, results.Count); for (var index = 0; index < results.Count; index++) { @@ -279,7 +285,7 @@ public async Task StandardizeObservationShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeObservationAsync(request); + var result = await _api.Terminology.StandardizeObservationAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -288,7 +294,7 @@ public async Task StandardizeObservationShouldStandardizeBatch() { var requests = StandardizeObservationCases.Select(row => row.Request).ToList(); var expected = StandardizeObservationCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeObservationAsync(requests); + var results = await _api.Terminology.StandardizeObservationAsync(requests); Assert.Equal(2, results.Count); for (var index = 0; index < results.Count; index++) { @@ -303,7 +309,7 @@ public async Task StandardizeProcedureShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeProcedureAsync(request); + var result = await _api.Terminology.StandardizeProcedureAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -312,7 +318,7 @@ public async Task StandardizeProcedureShouldStandardizeBatch() { var requests = StandardizeProcedureCases.Select(row => row.Request).ToList(); var expected = StandardizeProcedureCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeProcedureAsync(requests); + var results = await _api.Terminology.StandardizeProcedureAsync(requests); Assert.Equal(2, results.Count); for (var index = 0; index < results.Count; index++) { @@ -327,7 +333,7 @@ public async Task StandardizeRadiologyShouldStandardizeSingleRequest( string expectedCode ) { - var result = await Api.Terminology.StandardizeRadiologyAsync(request); + var result = await _api.Terminology.StandardizeRadiologyAsync(request); Assert.Contains(result.Coding, coding => coding.Code == expectedCode); } @@ -336,7 +342,7 @@ public async Task StandardizeRadiologyShouldStandardizeBatch() { var requests = StandardizeRadiologyCases.Select(row => row.Request).ToList(); var expected = StandardizeRadiologyCases.Select(row => row.ExpectedCode).ToList(); - var results = await Api.Terminology.StandardizeRadiologyAsync(requests); + var results = await _api.Terminology.StandardizeRadiologyAsync(requests); Assert.Equal(2, results.Count); for (var index = 0; index < results.Count; index++) { @@ -347,7 +353,7 @@ public async Task StandardizeRadiologyShouldStandardizeBatch() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task StandardizeBundleShouldStandardize() { - var result = await Api.Terminology.StandardizeBundleAsync(LiveTestData.R4Bundle); + var result = await _api.Terminology.StandardizeBundleAsync(LiveTestData.R4Bundle); Assert.NotNull(result.Entry); Assert.NotEmpty(result.Entry); } @@ -355,7 +361,7 @@ public async Task StandardizeBundleShouldStandardize() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertHl7ToFhirR4WithoutPatientShouldConvert() { - var result = await Api.Convert.Hl7ToFhirR4Async( + var result = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } ); Assert.NotNull(result.Entry); @@ -365,7 +371,7 @@ public async Task ConvertHl7ToFhirR4WithoutPatientShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertHl7ToFhirR4WithPatientShouldConvert() { - var result = await Api.Convert.Hl7ToFhirR4Async( + var result = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7, PatientId = "12/34" } ); var patient = GetEntryResourceByType( @@ -378,7 +384,7 @@ public async Task ConvertHl7ToFhirR4WithPatientShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertHl7ToFhirR4WithPatientIdentifierAndSystemShouldConvert() { - var result = await Api.Convert.Hl7ToFhirR4Async( + var result = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7, @@ -403,7 +409,7 @@ public async Task ConvertHl7ToFhirR4WithPatientIdentifierAndSystemShouldConvert( [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertHl7ToFhirR4WithTimezoneShouldConvert() { - var result = await Api.Convert.Hl7ToFhirR4Async( + var result = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7, Tz = "America/New_York" } ); var encounter = GetEntryResourceByType( @@ -416,7 +422,7 @@ public async Task ConvertHl7ToFhirR4WithTimezoneShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertHl7ToFhirR4WithoutTimezoneShouldPresumeUtc() { - var result = await Api.Convert.Hl7ToFhirR4Async( + var result = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } ); var encounter = GetEntryResourceByType( @@ -446,13 +452,13 @@ public async Task ConvertHl7ToFhirR4WithLabProcessingHintShouldConvert() OBX|9|ST|^PLATELETS^LAB|1|125|K/UL|130-400|L|||F|||202203091347|R^ROUTINE LAB|2222^ORDERED,BY| """; - var hintedResult = await Api.Convert.Hl7ToFhirR4Async( + var hintedResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "lab" } ); - var unhintedResult = await Api.Convert.Hl7ToFhirR4Async( + var unhintedResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content } ); - var defaultResult = await Api.Convert.Hl7ToFhirR4Async( + var defaultResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "default" } ); @@ -476,13 +482,13 @@ public async Task ConvertHl7ToFhirR4WithTranscriptionProcessingHintShouldConvert OBX|4|ST|Dictation TS|2|Dictated by: Tue Mar 18, 2025 1:06:45 PM EDT [INTERFACE, INCOMING RADIANT IMAGE AVAILABILITY]||||||Final|||||E175762^MILLER^AMANDA^^^^^^PROVID^^^^PROVID^^^^^^^^RT||||||||| """; - var hintedResult = await Api.Convert.Hl7ToFhirR4Async( + var hintedResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "transcription" } ); - var unhintedResult = await Api.Convert.Hl7ToFhirR4Async( + var unhintedResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content } ); - var defaultResult = await Api.Convert.Hl7ToFhirR4Async( + var defaultResult = await _api.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = content, ProcessingHint = "default" } ); @@ -494,7 +500,7 @@ public async Task ConvertHl7ToFhirR4WithTranscriptionProcessingHintShouldConvert [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToFhirR4WithoutPatientShouldConvert() { - var result = await Api.Convert.CdaToFhirR4Async( + var result = await _api.Convert.CdaToFhirR4Async( new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda } ); Assert.NotEmpty(result.Entry); @@ -503,7 +509,7 @@ public async Task ConvertCdaToFhirR4WithoutPatientShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToFhirR4WithIncludeOriginalCdaShouldConvert() { - var result = await Api.Convert.CdaToFhirR4Async( + var result = await _api.Convert.CdaToFhirR4Async( new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, IncludeOriginalCda = true } ); Assert.Contains( @@ -518,7 +524,7 @@ public async Task ConvertCdaToFhirR4WithIncludeOriginalCdaShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToFhirR4WithIncludeStandardizedCdaShouldConvert() { - var result = await Api.Convert.CdaToFhirR4Async( + var result = await _api.Convert.CdaToFhirR4Async( new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, @@ -537,7 +543,7 @@ public async Task ConvertCdaToFhirR4WithIncludeStandardizedCdaShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToFhirR4WithPatientShouldConvert() { - var result = await Api.Convert.CdaToFhirR4Async( + var result = await _api.Convert.CdaToFhirR4Async( new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, PatientId = "1234" } ); var patient = GetEntryResourceByType( @@ -550,7 +556,7 @@ public async Task ConvertCdaToFhirR4WithPatientShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToFhirR4WithPatientIdentifierAndSystemShouldConvert() { - var result = await Api.Convert.CdaToFhirR4Async( + var result = await _api.Convert.CdaToFhirR4Async( new ConvertCdaToFhirR4Request { Content = LiveTestData.Cda, @@ -575,7 +581,7 @@ public async Task ConvertCdaToFhirR4WithPatientIdentifierAndSystemShouldConvert( [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToPdfShouldConvert() { - var result = await Api.Convert.CdaToPdfAsync( + var result = await _api.Convert.CdaToPdfAsync( new ConvertCdaToPdfRequest { Content = LiveTestData.Cda } ); Assert.Equal(PdfMagicNumber, result.Take(4).ToArray()); @@ -584,7 +590,7 @@ public async Task ConvertCdaToPdfShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertFhirR4ToCdaShouldConvert() { - var result = await Api.Convert.FhirR4ToCdaAsync( + var result = await _api.Convert.FhirR4ToCdaAsync( new ConvertFhirR4ToCdaRequest { Content = LiveTestData.R4Bundle } ); Assert.StartsWith("( result, @@ -674,7 +680,7 @@ public async Task ConvertCombinedFhirR4BundlesWithPatientIdentifierAndSystemShou [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertX12ToFhirR4ShouldReturnBundle() { - var result = await Api.Convert.X12ToFhirR4Async( + var result = await _api.Convert.X12ToFhirR4Async( new ConvertX12ToFhirR4Request { Content = LiveTestData.X12Document } ); Assert.NotEmpty(result.Entry); @@ -683,7 +689,7 @@ public async Task ConvertX12ToFhirR4ShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertX12ToFhirR4WithPatientShouldReturnBundle() { - var result = await Api.Convert.X12ToFhirR4Async( + var result = await _api.Convert.X12ToFhirR4Async( new ConvertX12ToFhirR4Request { Content = LiveTestData.X12Document, @@ -700,7 +706,7 @@ public async Task ConvertX12ToFhirR4WithPatientShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertX12ToFhirR4WithPatientIdentifierAndSystemShouldConvert() { - var result = await Api.Convert.X12ToFhirR4Async( + var result = await _api.Convert.X12ToFhirR4Async( new ConvertX12ToFhirR4Request { Content = LiveTestData.X12Document, @@ -725,7 +731,7 @@ public async Task ConvertX12ToFhirR4WithPatientIdentifierAndSystemShouldConvert( [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4CodeSystemShouldReturnCodeSystem() { - var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + var result = await _api.Terminology.GetFhirR4CodeSystemAsync( new GetFhirR4CodeSystemRequest { CodeSystem = "SNOMED" } ); Assert.NotEmpty(result.Concept); @@ -734,7 +740,7 @@ public async Task GetFhirR4CodeSystemShouldReturnCodeSystem() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4CodeSystemWithPageShouldReturnCodeSystem() { - var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + var result = await _api.Terminology.GetFhirR4CodeSystemAsync( new GetFhirR4CodeSystemRequest { CodeSystem = "SNOMED", @@ -748,7 +754,7 @@ public async Task GetFhirR4CodeSystemWithPageShouldReturnCodeSystem() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4CodeSystemWithSearchShouldReturnCodeSystem() { - var result = await Api.Terminology.GetFhirR4CodeSystemAsync( + var result = await _api.Terminology.GetFhirR4CodeSystemAsync( new GetFhirR4CodeSystemRequest { CodeSystem = "ICD-10-CM", @@ -763,14 +769,14 @@ public async Task GetFhirR4CodeSystemWithSearchShouldReturnCodeSystem() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task SummarizeFhirR4CodeSystemsShouldReturnBundle() { - var result = await Api.Terminology.SummarizeFhirR4CodeSystemsAsync(); + var result = await _api.Terminology.SummarizeFhirR4CodeSystemsAsync(); Assert.NotEmpty(result.Entry); } [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ConceptMapsShouldReturnBundle() { - var result = await Api.Terminology.GetFhirR4ConceptMapsAsync(); + var result = await _api.Terminology.GetFhirR4ConceptMapsAsync(); Assert.NotEmpty(result.Entry); Assert.All( result.Entry, @@ -782,7 +788,7 @@ public async Task GetFhirR4ConceptMapsShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task TranslateFhirR4ConceptMapWithCodeShouldTranslate() { - var result = await Api.Terminology.TranslateFhirR4ConceptMapAsync( + var result = await _api.Terminology.TranslateFhirR4ConceptMapAsync( new TranslateFhirR4ConceptMapRequest { Code = "119981000146107" } ); Assert.NotEmpty(result.Parameter); @@ -791,7 +797,7 @@ public async Task TranslateFhirR4ConceptMapWithCodeShouldTranslate() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task TranslateFhirR4ConceptMapWithCodeAndDomainShouldTranslate() { - var result = await Api.Terminology.TranslateFhirR4ConceptMapAsync( + var result = await _api.Terminology.TranslateFhirR4ConceptMapAsync( new TranslateFhirR4ConceptMapRequest { Code = "119981000146107", Domain = "Condition" } ); Assert.NotEmpty(result.Parameter); @@ -800,7 +806,7 @@ public async Task TranslateFhirR4ConceptMapWithCodeAndDomainShouldTranslate() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task SummarizeFhirR4ValueSetScopeShouldReturnBundle() { - var result = await Api.Terminology.SummarizeFhirR4ValueSetScopeAsync( + var result = await _api.Terminology.SummarizeFhirR4ValueSetScopeAsync( new SummarizeFhirR4ValueSetScopeRequest { Scope = "http://loinc.org" } ); Assert.NotEmpty(result.Entry); @@ -810,7 +816,7 @@ public async Task SummarizeFhirR4ValueSetScopeShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetShouldReturnValueSet() { - var result = await Api.Terminology.GetFhirR4ValueSetAsync( + var result = await _api.Terminology.GetFhirR4ValueSetAsync( new GetFhirR4ValueSetRequest { Id = "00987FA2EDADBD0E43DA59E171B80F99DBF832C69904489EE6F9E6450925E5A2", @@ -823,7 +829,7 @@ public async Task GetFhirR4ValueSetShouldReturnValueSet() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task SummarizeFhirR4ValueSetShouldReturnValueSet() { - var result = await Api.Terminology.SummarizeFhirR4ValueSetAsync( + var result = await _api.Terminology.SummarizeFhirR4ValueSetAsync( new SummarizeFhirR4ValueSetRequest { Id = "00987FA2EDADBD0E43DA59E171B80F99DBF832C69904489EE6F9E6450925E5A2", @@ -835,7 +841,7 @@ public async Task SummarizeFhirR4ValueSetShouldReturnValueSet() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetScopesShouldReturnValueSet() { - var result = await Api.Terminology.GetFhirR4ValueSetScopesAsync(); + var result = await _api.Terminology.GetFhirR4ValueSetScopesAsync(); Assert.NotNull(result.Compose?.Include); Assert.NotEmpty(result.Compose.Include); } @@ -844,7 +850,7 @@ public async Task GetFhirR4ValueSetScopesShouldReturnValueSet() public async Task GetFhirR4ValueSetsByScopeWithoutPaginationShouldRaise() { await Assert.ThrowsAsync(() => - Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + _api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { Scope = "http://loinc.org" } ) ); @@ -853,7 +859,7 @@ await Assert.ThrowsAsync(() => [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetsByScopeWithPageAndScopeShouldReturnBundle() { - var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + var result = await _api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { Scope = "http://loinc.org", @@ -867,7 +873,7 @@ public async Task GetFhirR4ValueSetsByScopeWithPageAndScopeShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetsByScopeWithPageAndNameShouldReturnBundle() { - var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + var result = await _api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { Name = "LP7839-6", @@ -881,7 +887,7 @@ public async Task GetFhirR4ValueSetsByScopeWithPageAndNameShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetsByScopeWithPageNameAndScopeShouldReturnBundle() { - var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + var result = await _api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { Name = "LP7839-6", @@ -896,7 +902,7 @@ public async Task GetFhirR4ValueSetsByScopeWithPageNameAndScopeShouldReturnBundl [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task GetFhirR4ValueSetsByScopeWithJustPageShouldReturnBundle() { - var result = await Api.Terminology.GetFhirR4ValueSetsByScopeAsync( + var result = await _api.Terminology.GetFhirR4ValueSetsByScopeAsync( new GetFhirR4ValueSetsByScopeRequest { PageNumber = 0, PageSize = 2 } ); Assert.NotEmpty(result.Entry); @@ -905,7 +911,7 @@ public async Task GetFhirR4ValueSetsByScopeWithJustPageShouldReturnBundle() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task SummarizeFhirR4CodeSystemShouldReturnCodeSystem() { - var result = await Api.Terminology.SummarizeFhirR4CodeSystemAsync( + var result = await _api.Terminology.SummarizeFhirR4CodeSystemAsync( new SummarizeFhirR4CodeSystemRequest { CodeSystem = "SNOMED" } ); Assert.True(result.Count > 0); @@ -931,14 +937,14 @@ public async Task GetAllFhirR4ValueSetsForCodesShouldReturnParameters() ], }; - var result = await Api.Terminology.GetAllFhirR4ValueSetsForCodesAsync(parameters); + var result = await _api.Terminology.GetAllFhirR4ValueSetsForCodesAsync(parameters); Assert.NotEmpty(result.Parameter); } [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertFhirDstu2ToFhirR4ShouldConvert() { - var result = await Api.Convert.FhirDstu2ToFhirR4Async( + var result = await _api.Convert.FhirDstu2ToFhirR4Async( new ConvertFhirDstu2ToFhirR4Request { Content = LiveTestData.Dstu2Bundle } ); var patient = GetEntryResourceByType( @@ -954,7 +960,7 @@ public async Task ConvertFhirDstu2ToFhirR4ShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertFhirStu3ToFhirR4ShouldConvert() { - var result = await Api.Convert.FhirStu3ToFhirR4Async( + var result = await _api.Convert.FhirStu3ToFhirR4Async( new ConvertFhirStu3ToFhirR4Request { Content = LiveTestData.Stu3Bundle } ); var patient = GetEntryResourceByType( @@ -970,7 +976,7 @@ public async Task ConvertFhirStu3ToFhirR4ShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertFhirR4ToHealthLakeShouldConvert() { - var result = await Api.Convert.FhirR4ToHealthLakeAsync( + var result = await _api.Convert.FhirR4ToHealthLakeAsync( new ConvertFhirR4ToHealthLakeRequest { Content = LiveTestData.R4Bundle } ); Assert.Equal(Hl7.Fhir.Model.BundleType.Collection, result.Type); @@ -981,7 +987,7 @@ public async Task ConvertFhirR4ToHealthLakeShouldConvert() [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ConvertCdaToHtmlShouldConvert() { - var result = await Api.Convert.CdaToHtmlAsync( + var result = await _api.Convert.CdaToHtmlAsync( new ConvertCdaToHtmlRequest { Content = LiveTestData.Cda } ); Assert.StartsWith("(() => timeoutApi.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } @@ -1081,4 +1088,9 @@ Hl7.Fhir.Model.ResourceType resourceType as TResource ?? throw new InvalidOperationException($"Expected resource type '{resourceType}'."); } + + public void Dispose() + { + _httpClient.Dispose(); + } } diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs index ca9429d..f9efa19 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveIdentityApiTests.cs @@ -2,9 +2,10 @@ namespace CareEvolution.Orchestrate.Tests; -public sealed class LiveIdentityApiTests +public sealed class LiveIdentityApiTests : IDisposable { - private static readonly IdentityApi Api = LiveClients.CreateIdentityApi(); + private readonly HttpClient _httpClient = new(); + private readonly IdentityApi _api; private const string DefaultSource = "source"; private static readonly Demographic Demographic = new() @@ -21,10 +22,15 @@ public sealed class LiveIdentityApiTests Version = 1, }; + public LiveIdentityApiTests() + { + _api = LiveClients.CreateIdentityApi(_httpClient); + } + [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task AddOrUpdateRecordShouldAddRecord() { - var response = await Api.AddOrUpdateRecordAsync( + var response = await _api.AddOrUpdateRecordAsync( new AddOrUpdateRecordRequest { Source = DefaultSource, @@ -39,7 +45,7 @@ public async Task AddOrUpdateRecordShouldAddRecord() [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task AddOrUpdateRecordWithUrlUnsafeIdentifierShouldAddRecord() { - var response = await Api.AddOrUpdateRecordAsync( + var response = await _api.AddOrUpdateRecordAsync( new AddOrUpdateRecordRequest { Source = DefaultSource, @@ -54,7 +60,7 @@ public async Task AddOrUpdateRecordWithUrlUnsafeIdentifierShouldAddRecord() [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task AddOrUpdateBlindedRecordShouldAddRecord() { - var response = await Api.AddOrUpdateBlindedRecordAsync( + var response = await _api.AddOrUpdateBlindedRecordAsync( new AddOrUpdateBlindedRecordRequest { Source = DefaultSource, @@ -69,7 +75,7 @@ public async Task AddOrUpdateBlindedRecordShouldAddRecord() [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task AddOrUpdateBlindedRecordWithUrlUnsafeIdentifierShouldAddRecord() { - var response = await Api.AddOrUpdateBlindedRecordAsync( + var response = await _api.AddOrUpdateBlindedRecordAsync( new AddOrUpdateBlindedRecordRequest { Source = DefaultSource, @@ -85,7 +91,7 @@ public async Task AddOrUpdateBlindedRecordWithUrlUnsafeIdentifierShouldAddRecord public async Task GetPersonByRecordShouldMatch() { var (person, identifier) = await CreateRandomRecordAsync(); - var response = await Api.GetPersonByRecordAsync( + var response = await _api.GetPersonByRecordAsync( new CareEvolution.Orchestrate.Identity.Record { Source = DefaultSource, @@ -99,14 +105,14 @@ public async Task GetPersonByRecordShouldMatch() public async Task GetPersonByIdShouldMatch() { var (person, _) = await CreateRandomRecordAsync(); - var response = await Api.GetPersonByIdAsync(new GetPersonByIdRequest { Id = person.Id }); + var response = await _api.GetPersonByIdAsync(new GetPersonByIdRequest { Id = person.Id }); Assert.Equal(person.Id, response.Id); } [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task MatchDemographicsShouldMatch() { - var response = await Api.MatchDemographicsAsync(Demographic); + var response = await _api.MatchDemographicsAsync(Demographic); Assert.NotNull(response); Assert.NotNull(response.MatchingPersons); } @@ -114,7 +120,7 @@ public async Task MatchDemographicsShouldMatch() [LiveFact(LiveTestEnvironment.IdentityApiKey, LiveTestEnvironment.IdentityUrl)] public async Task MatchBlindedDemographicsShouldMatch() { - var response = await Api.MatchBlindedDemographicsAsync(BlindedDemographic); + var response = await _api.MatchBlindedDemographicsAsync(BlindedDemographic); Assert.NotNull(response); Assert.NotNull(response.MatchingPersons); } @@ -123,7 +129,7 @@ public async Task MatchBlindedDemographicsShouldMatch() public async Task DeleteRecordShouldDelete() { var (person, identifier) = await CreateRandomRecordAsync(); - var response = await Api.DeleteRecordAsync( + var response = await _api.DeleteRecordAsync( new CareEvolution.Orchestrate.Identity.Record { Source = DefaultSource, @@ -144,7 +150,7 @@ public async Task AddMatchGuidanceShouldReportChangedPersons() var (firstPerson, firstIdentifier) = await CreateRandomRecordAsync(); var (secondPerson, secondIdentifier) = await CreateRandomRecordAsync(); - var response = await Api.AddMatchGuidanceAsync( + var response = await _api.AddMatchGuidanceAsync( new AddMatchGuidanceRequest { RecordOne = new CareEvolution.Orchestrate.Identity.Record @@ -184,7 +190,7 @@ public async Task RemoveMatchGuidanceShouldSeparatePersons() Identifier = secondIdentifier, }; - await Api.AddMatchGuidanceAsync( + await _api.AddMatchGuidanceAsync( new AddMatchGuidanceRequest { RecordOne = recordOne, @@ -194,7 +200,7 @@ await Api.AddMatchGuidanceAsync( } ); - var response = await Api.RemoveMatchGuidanceAsync( + var response = await _api.RemoveMatchGuidanceAsync( new MatchGuidanceRequest { RecordOne = recordOne, @@ -217,7 +223,7 @@ await Api.AddMatchGuidanceAsync( public async Task MonitoringIdentifierMetricsShouldHaveMetrics() { await CreateRandomRecordAsync(); - var response = await Api.Monitoring.IdentifierMetricsAsync(); + var response = await _api.Monitoring.IdentifierMetricsAsync(); Assert.False(string.IsNullOrWhiteSpace(response.Refreshed)); Assert.True(response.TotalRecordCount > 0); @@ -238,7 +244,7 @@ public async Task MonitoringOverlapMetricsShouldHaveMetrics() await CreateRandomRecordAsync(); await CreateRandomRecordAsync(); - var response = await Api.Monitoring.OverlapMetricsAsync(); + var response = await _api.Monitoring.OverlapMetricsAsync(); Assert.NotNull(response.DatasourceOverlapRecords); Assert.Contains( response.DatasourceOverlapRecords, @@ -251,10 +257,10 @@ record => record.DatasourceB == DefaultSource Assert.True(response.DatasourceOverlapRecords[0].OverlapCount > 0); } - private static async Task<(Person Person, string Identifier)> CreateRandomRecordAsync() + private async Task<(Person Person, string Identifier)> CreateRandomRecordAsync() { var identifier = Guid.NewGuid().ToString(); - var response = await Api.AddOrUpdateRecordAsync( + var response = await _api.AddOrUpdateRecordAsync( new AddOrUpdateRecordRequest { Source = DefaultSource, @@ -264,4 +270,9 @@ record => record.DatasourceB == DefaultSource ); return (response.MatchedPerson!, identifier); } + + public void Dispose() + { + _httpClient.Dispose(); + } } diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs index adf183b..c9999f2 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveLocalHashingApiTests.cs @@ -2,9 +2,10 @@ namespace CareEvolution.Orchestrate.Tests; -public sealed class LiveLocalHashingApiTests +public sealed class LiveLocalHashingApiTests : IDisposable { - private static readonly LocalHashingApi Api = LiveClients.CreateLocalHashingApi(); + private readonly HttpClient _httpClient = new(); + private readonly LocalHashingApi _api; private static readonly Demographic Demographic = new() { @@ -14,10 +15,15 @@ public sealed class LiveLocalHashingApiTests Gender = "male", }; + public LiveLocalHashingApiTests() + { + _api = LiveClients.CreateLocalHashingApi(_httpClient); + } + [LiveFact(LiveTestEnvironment.LocalHashingUrl)] public async Task HashShouldHashByDemographic() { - var response = await Api.HashAsync(Demographic); + var response = await _api.HashAsync(Demographic); Assert.True(response.Version > 0); Assert.Equal([], response.Advisories?.InvalidDemographicFields ?? []); @@ -26,7 +32,7 @@ public async Task HashShouldHashByDemographic() [LiveFact(LiveTestEnvironment.LocalHashingUrl)] public async Task HashWithInvalidDemographicFieldsShouldReturnAdvisories() { - var response = await Api.HashAsync( + var response = await _api.HashAsync( new Demographic { FirstName = Demographic.FirstName, @@ -38,4 +44,9 @@ public async Task HashWithInvalidDemographicFieldsShouldReturnAdvisories() Assert.True(response.Version > 0); Assert.Equal(["dob"], response.Advisories?.InvalidDemographicFields ?? []); } + + public void Dispose() + { + _httpClient.Dispose(); + } } From e2c685795c2d87a3b5e4d27fd47cd103941e13cd Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:41:52 -0400 Subject: [PATCH 14/21] Add Unit Tests for BuildUri See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012302316 --- .../ApiSurfaceTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs index 1c5d6bf..cc00077 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -113,6 +113,27 @@ public async Task AdvancedTransportShouldSupportTextResponses() Assert.Equal("text/html", handler.LastRequest!.Headers["Accept"].Single()); } + [Theory] + [InlineData("https://api.example.com", "/custom/v1/ping", "https://api.example.com/custom/v1/ping")] + [InlineData("https://api.example.com/", "custom/v1/ping", "https://api.example.com/custom/v1/ping")] + [InlineData("https://api.example.com/", "/custom/v1/ping", "https://api.example.com/custom/v1/ping")] + public async Task AdvancedTransportShouldNormalizeBaseUrlAndPath( + string baseUrl, + string path, + string expectedUrl + ) + { + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) + ); + using var httpClient = new HttpClient(handler); + var api = new OrchestrateApi(httpClient, new OrchestrateClientOptions { BaseUrl = baseUrl }); + + _ = await api.HttpHandler.GetJsonAsync(path); + + Assert.Equal(expectedUrl, handler.LastRequest!.RequestUri!.AbsoluteUri); + } + [Fact] public void AddOrchestrateApiShouldRegisterIOrchestrateApi() { From 297cf2aa7a2035f4f467128ffd95866e4219638b Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 16:57:27 -0400 Subject: [PATCH 15/21] Parse Operational Outcomes See - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012364226 - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012368009 --- .../EnvironmentConfiguration.cs | 36 +++++- .../Exceptions/OrchestrateClientException.cs | 6 +- .../OrchestrateHttpClient.cs | 101 +++++++++++----- .../ConfigurationTests.cs | 112 +++++++++++++++++- 4 files changed, 218 insertions(+), 37 deletions(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs index a3eabff..b9c3a5a 100644 --- a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -18,13 +18,16 @@ internal static class EnvironmentConfiguration public static ResolvedConfiguration Resolve(OrchestrateClientOptions? options) { - return new ResolvedConfiguration( + var additionalHeaders = GetAdditionalHeaders(); + var configuration = new ResolvedConfiguration( BaseUrl: GetPriorityOrMissing(options?.BaseUrl, BaseUrlEnvironmentVariable) ?? DefaultBaseUrl, TimeoutMs: GetTimeout(options?.TimeoutMs), - AdditionalHeaders: GetAdditionalHeaders(), + AdditionalHeaders: additionalHeaders, ApiKey: GetPriority(options?.ApiKey, ApiKeyEnvironmentVariable) ); + ValidateExactlyOneAuthenticationHeader(configuration); + return configuration; } public static ResolvedConfiguration Resolve(IdentityApiOptions? options) @@ -38,15 +41,18 @@ public static ResolvedConfiguration Resolve(IdentityApiOptions? options) } var metricsKey = GetPriority(options?.MetricsKey, IdentityMetricsKeyEnvironmentVariable); - return new ResolvedConfiguration( + var additionalHeaders = GetAdditionalHeaders(); + var configuration = new ResolvedConfiguration( BaseUrl: url, TimeoutMs: GetTimeout(options?.TimeoutMs), - AdditionalHeaders: GetAdditionalHeaders(), + AdditionalHeaders: additionalHeaders, ApiKey: GetPriority(options?.ApiKey, IdentityApiKeyEnvironmentVariable), Authorization: string.IsNullOrWhiteSpace(metricsKey) ? null : $"Basic {NormalizeBasicCredential(metricsKey)}" ); + ValidateExactlyOneAuthenticationHeader(configuration); + return configuration; } public static ResolvedConfiguration Resolve(LocalHashingApiOptions? options) @@ -146,4 +152,26 @@ private static string NormalizeBasicCredential(string metricsKey) return normalized; } + + private static void ValidateExactlyOneAuthenticationHeader(ResolvedConfiguration configuration) + { + var hasApiKey = HasConfiguredValue(configuration.ApiKey) + || HasConfiguredHeader(configuration.AdditionalHeaders, "x-api-key"); + var hasAuthorization = HasConfiguredValue(configuration.Authorization) + || HasConfiguredHeader(configuration.AdditionalHeaders, "Authorization"); + + if (hasApiKey == hasAuthorization) + { + throw new ArgumentException( + "Exactly one authentication header must be configured: either 'x-api-key' or 'Authorization'." + ); + } + } + + private static bool HasConfiguredHeader( + IReadOnlyDictionary headers, + string headerName + ) => headers.TryGetValue(headerName, out var value) && HasConfiguredValue(value); + + private static bool HasConfiguredValue(string? value) => !string.IsNullOrWhiteSpace(value); } diff --git a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs index aae19f5..8fd8897 100644 --- a/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs +++ b/dotnet/src/CareEvolution.Orchestrate/Exceptions/OrchestrateClientException.cs @@ -1,4 +1,5 @@ using System.Net; +using Hl7.Fhir.Model; namespace CareEvolution.Orchestrate.Exceptions; @@ -8,7 +9,8 @@ namespace CareEvolution.Orchestrate.Exceptions; public sealed class OrchestrateClientException( string responseText, IReadOnlyList issues, - HttpStatusCode statusCode + HttpStatusCode statusCode, + OperationOutcome? operationOutcome = null ) : OrchestrateHttpException( issues.Count > 0 ? $"\n * {string.Join(" \n * ", issues)}" : responseText, @@ -18,4 +20,6 @@ HttpStatusCode statusCode public string ResponseText { get; } = responseText; public IReadOnlyList Issues { get; } = issues; + + public OperationOutcome? OperationOutcome { get; } = operationOutcome; } diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs index ded7b3c..e3e72d1 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -339,21 +339,79 @@ CancellationToken cancellationToken var responseText = await response .Content.ReadAsStringAsync(cancellationToken) .ConfigureAwait(false); - var issues = ReadOperationalOutcomes(responseText); + var operationOutcome = TryParseOperationOutcome(responseText); + var issues = ReadOperationalOutcomes(responseText, operationOutcome); if ((int)response.StatusCode >= 400 && (int)response.StatusCode < 600) { - return new OrchestrateClientException(responseText, issues, response.StatusCode); + return new OrchestrateClientException( + responseText, + issues, + response.StatusCode, + operationOutcome + ); } return new OrchestrateHttpException(responseText, response.StatusCode); } - private static IReadOnlyList ReadOperationalOutcomes(string responseText) + private static IReadOnlyList ReadOperationalOutcomes( + string responseText, + Hl7.Fhir.Model.OperationOutcome? operationOutcome + ) { + var fhirOutcomes = ReadFhirOutcomes(operationOutcome); + if (fhirOutcomes.Count > 0) + { + return fhirOutcomes; + } + var outcomes = ReadJsonOutcomes(responseText); return outcomes.Count > 0 ? outcomes : [responseText]; } + private static Hl7.Fhir.Model.OperationOutcome? TryParseOperationOutcome(string responseText) + { + if (!responseText.Contains("\"resourceType\"", StringComparison.Ordinal)) + { + return null; + } + + try + { + return FhirJsonParser.Parse(responseText); + } + catch + { + return null; + } + } + + private static List ReadFhirOutcomes(Hl7.Fhir.Model.OperationOutcome? operationOutcome) + { + if (operationOutcome?.Issue is null || operationOutcome.Issue.Count == 0) + { + return []; + } + + return operationOutcome + .Issue.Select(issue => + { + var severity = issue.Severity?.ToString()?.ToLowerInvariant() ?? string.Empty; + var code = issue.Code?.ToString()?.ToLowerInvariant() ?? string.Empty; + var diagnostics = issue.Diagnostics ?? string.Empty; + var details = GetIssueDetailString(issue.Details); + var prefix = $"{severity}: {code}"; + var message = string.Join( + "; ", + new[] { details, diagnostics }.Where(static value => + !string.IsNullOrWhiteSpace(value) + ) + ); + return string.IsNullOrWhiteSpace(message) ? prefix : $"{prefix} - {message}"; + }) + .ToList(); + } + private static List ReadJsonOutcomes(string responseText) { try @@ -369,26 +427,7 @@ private static List ReadJsonOutcomes(string responseText) && issueNode is JsonArray issuesArray ) { - return issuesArray - .OfType() - .Select(issue => - { - var severity = issue["severity"]?.GetValue() ?? string.Empty; - var code = issue["code"]?.GetValue() ?? string.Empty; - var diagnostics = issue["diagnostics"]?.GetValue() ?? string.Empty; - var details = GetIssueDetailString(issue["details"]); - var prefix = $"{severity}: {code}"; - var message = string.Join( - "; ", - new[] { details, diagnostics }.Where(static value => - !string.IsNullOrWhiteSpace(value) - ) - ); - return string.IsNullOrWhiteSpace(message) - ? prefix - : $"{prefix} - {message}"; - }) - .ToList(); + return []; } if ( @@ -409,24 +448,24 @@ private static List ReadJsonOutcomes(string responseText) return []; } - private static string GetIssueDetailString(JsonNode? detailNode) + private static string GetIssueDetailString(Hl7.Fhir.Model.CodeableConcept? detail) { - if (detailNode is not JsonObject detailObject) + if (detail is null) { return string.Empty; } - if (detailObject["text"]?.GetValue() is { Length: > 0 } text) + if (!string.IsNullOrWhiteSpace(detail.Text)) { - return text; + return detail.Text; } - if (detailObject["coding"] is JsonArray codingArray) + if (detail.Coding is not null) { - foreach (var codingNode in codingArray.OfType()) + foreach (var coding in detail.Coding) { - var code = codingNode["code"]?.GetValue(); - var display = codingNode["display"]?.GetValue(); + var code = coding.Code; + var display = coding.Display; var joined = string.Join( ": ", new[] { code, display }.Where(static value => !string.IsNullOrWhiteSpace(value)) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 369890a..51f8f61 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -88,6 +88,72 @@ public void OrchestrateApiShouldThrowForInvalidTimeoutEnvironmentVariable() Assert.Contains("ORCHESTRATE_TIMEOUT_MS", exception.Message); } + [Fact] + public void OrchestrateApiShouldRequireExactlyOneAuthenticationHeader() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_API_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); + + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new OrchestrateApi(httpClient)); + + Assert.Contains("Exactly one authentication header", exception.Message); + } + + [Fact] + public void OrchestrateApiShouldThrowWhenBothAuthenticationHeadersAreConfigured() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_API_KEY"] = "env-api-key", + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = """{"Authorization":"Bearer token"}""", + } + ); + + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new OrchestrateApi(httpClient)); + + Assert.Contains("Exactly one authentication header", exception.Message); + } + + [Fact] + public async Task OrchestrateApiShouldAllowAuthorizationFromAdditionalHeaders() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_API_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = """{"Authorization":"Bearer token"}""", + } + ); + + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) + ); + using var httpClient = new HttpClient(handler); + var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + ); + + await api.Terminology.StandardizeConditionAsync( + new StandardizeRequest { Code = "123", System = "SNOMED" } + ); + + Assert.Equal("Bearer token", handler.LastRequest!.Headers["Authorization"].Single()); + Assert.False(handler.LastRequest.Headers.ContainsKey("x-api-key")); + } + [Fact] public void IdentityApiShouldRequireUrl() { @@ -116,6 +182,7 @@ string expectedAuthorization new Dictionary { ["ORCHESTRATE_IDENTITY_URL"] = "https://identity.example.com", + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, } ); @@ -134,6 +201,47 @@ string expectedAuthorization Assert.Equal(expectedAuthorization, handler.LastRequest!.Headers["Authorization"].Single()); } + [Fact] + public void IdentityApiShouldRequireExactlyOneAuthenticationHeader() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_URL"] = "https://identity.example.com", + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); + + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new IdentityApi(httpClient)); + + Assert.Contains("Exactly one authentication header", exception.Message); + } + + [Fact] + public void IdentityApiShouldThrowWhenBothAuthenticationHeadersAreConfigured() + { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_URL"] = "https://identity.example.com", + ["ORCHESTRATE_IDENTITY_API_KEY"] = "identity-api-key", + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = "metrics-key", + } + ); + + using var httpClient = new HttpClient( + new FakeHttpMessageHandler((_, _) => throw new NotImplementedException()) + ); + var exception = Assert.Throws(() => new IdentityApi(httpClient)); + + Assert.Contains("Exactly one authentication header", exception.Message); + } + [Fact] public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() { @@ -141,7 +249,7 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() (_, _) => Task.FromResult( FakeResponses.Json( - """{"issue":[{"severity":"error","code":"invalid","diagnostics":"Expected a Bundle but found a Patient"}]}""", + """{"resourceType":"OperationOutcome","issue":[{"severity":"error","code":"invalid","diagnostics":"Expected a Bundle but found a Patient"}]}""", HttpStatusCode.BadRequest ) ) @@ -164,5 +272,7 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() exception.Message ); Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + Assert.NotNull(exception.OperationOutcome); + Assert.Single(exception.OperationOutcome!.Issue); } } From e4d1acf75d1a1966721fb25f99da8eaf1652980f Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 17:00:05 -0400 Subject: [PATCH 16/21] Ensure Unparseable JSON is Returned as ResponseText See https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3012375883 --- .../OrchestrateHttpClient.cs | 6 ++-- .../ConfigurationTests.cs | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs index e3e72d1..976635e 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -419,7 +419,7 @@ private static List ReadJsonOutcomes(string responseText) var root = JsonNode.Parse(responseText)?.AsObject(); if (root is null) { - return []; + return [responseText]; } if ( @@ -427,7 +427,7 @@ private static List ReadJsonOutcomes(string responseText) && issueNode is JsonArray issuesArray ) { - return []; + return [responseText]; } if ( @@ -442,7 +442,7 @@ private static List ReadJsonOutcomes(string responseText) } catch { - return []; + return [responseText]; } return []; diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 51f8f61..e2a4b3d 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -275,4 +275,34 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() Assert.NotNull(exception.OperationOutcome); Assert.Single(exception.OperationOutcome!.Issue); } + + [Fact] + public async Task MalformedJsonHttpErrorsShouldPreserveRawResponseText() + { + const string responseText = """{"oops":"""; + var handler = new FakeHttpMessageHandler( + (_, _) => Task.FromResult(FakeResponses.Text(responseText, "application/json", HttpStatusCode.BadRequest)) + ); + + using var httpClient = new HttpClient(handler); + var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } + ); + + var exception = await Assert.ThrowsAsync(() => + api.Terminology.StandardizeConditionAsync( + new StandardizeRequest { Code = "123", System = "SNOMED" } + ) + ); + + Assert.Equal(responseText, exception.ResponseText); + Assert.Contains(responseText, exception.Issues); + Assert.Contains(responseText, exception.Message); + Assert.Null(exception.OperationOutcome); + } } From 84ffc38377ba869412850b310897dc8e10aa745a Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 17:00:30 -0400 Subject: [PATCH 17/21] Format --- .../EnvironmentConfiguration.cs | 6 +++-- .../ApiSurfaceTests.cs | 23 +++++++++++++++---- .../ConfigurationTests.cs | 10 ++++---- .../LiveApiTests.cs | 9 ++++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs index b9c3a5a..1e3e4b8 100644 --- a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -155,9 +155,11 @@ private static string NormalizeBasicCredential(string metricsKey) private static void ValidateExactlyOneAuthenticationHeader(ResolvedConfiguration configuration) { - var hasApiKey = HasConfiguredValue(configuration.ApiKey) + var hasApiKey = + HasConfiguredValue(configuration.ApiKey) || HasConfiguredHeader(configuration.AdditionalHeaders, "x-api-key"); - var hasAuthorization = HasConfiguredValue(configuration.Authorization) + var hasAuthorization = + HasConfiguredValue(configuration.Authorization) || HasConfiguredHeader(configuration.AdditionalHeaders, "Authorization"); if (hasApiKey == hasAuthorization) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs index cc00077..ca143cf 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -114,9 +114,21 @@ public async Task AdvancedTransportShouldSupportTextResponses() } [Theory] - [InlineData("https://api.example.com", "/custom/v1/ping", "https://api.example.com/custom/v1/ping")] - [InlineData("https://api.example.com/", "custom/v1/ping", "https://api.example.com/custom/v1/ping")] - [InlineData("https://api.example.com/", "/custom/v1/ping", "https://api.example.com/custom/v1/ping")] + [InlineData( + "https://api.example.com", + "/custom/v1/ping", + "https://api.example.com/custom/v1/ping" + )] + [InlineData( + "https://api.example.com/", + "custom/v1/ping", + "https://api.example.com/custom/v1/ping" + )] + [InlineData( + "https://api.example.com/", + "/custom/v1/ping", + "https://api.example.com/custom/v1/ping" + )] public async Task AdvancedTransportShouldNormalizeBaseUrlAndPath( string baseUrl, string path, @@ -127,7 +139,10 @@ string expectedUrl (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) ); using var httpClient = new HttpClient(handler); - var api = new OrchestrateApi(httpClient, new OrchestrateClientOptions { BaseUrl = baseUrl }); + var api = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { BaseUrl = baseUrl } + ); _ = await api.HttpHandler.GetJsonAsync(path); diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index e2a4b3d..48c9f18 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -59,10 +59,7 @@ public async Task OrchestrateApiShouldTreatWhitespaceBaseUrlAsMissing() (_, _) => Task.FromResult(FakeResponses.Json("""{"coding":[]}""")) ); using var httpClient = new HttpClient(handler); - var api = new OrchestrateApi( - httpClient, - new OrchestrateClientOptions { BaseUrl = " " } - ); + var api = new OrchestrateApi(httpClient, new OrchestrateClientOptions { BaseUrl = " " }); await api.Terminology.StandardizeConditionAsync( new StandardizeRequest { Code = "123", System = "SNOMED" } @@ -281,7 +278,10 @@ public async Task MalformedJsonHttpErrorsShouldPreserveRawResponseText() { const string responseText = """{"oops":"""; var handler = new FakeHttpMessageHandler( - (_, _) => Task.FromResult(FakeResponses.Text(responseText, "application/json", HttpStatusCode.BadRequest)) + (_, _) => + Task.FromResult( + FakeResponses.Text(responseText, "application/json", HttpStatusCode.BadRequest) + ) ); using var httpClient = new HttpClient(handler); diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs index 7f9562f..b8355af 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/LiveApiTests.cs @@ -181,7 +181,9 @@ ClassifyMedicationRequest request [LiveFact(LiveTestEnvironment.OrchestrateApiKey)] public async Task ClassifyMedicationShouldClassifyBatch() { - var results = await _api.Terminology.ClassifyMedicationAsync(ClassifyMedicationRequestItems); + var results = await _api.Terminology.ClassifyMedicationAsync( + ClassifyMedicationRequestItems + ); Assert.Equal(2, results.Count); Assert.All(results, result => Assert.True(result.RxNormGeneric)); } @@ -1067,7 +1069,10 @@ public async Task ConvertFhirR4ToManifestWithDelimiterShouldHaveCsvsAndExpectedD public async Task WithTimeoutShouldTimeout() { using var httpClient = new HttpClient(); - var timeoutApi = new OrchestrateApi(httpClient, new OrchestrateClientOptions { TimeoutMs = 1 }); + var timeoutApi = new OrchestrateApi( + httpClient, + new OrchestrateClientOptions { TimeoutMs = 1 } + ); await Assert.ThrowsAnyAsync(() => timeoutApi.Convert.Hl7ToFhirR4Async( new ConvertHl7ToFhirR4Request { Content = LiveTestData.Hl7 } From c9793aa97cc3709bfaf1cdab5aad0662dab69b04 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Tue, 31 Mar 2026 17:02:20 -0400 Subject: [PATCH 18/21] Fix Header Auth Tests for Identity --- .../IdentityApiTests.cs | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs index 8af6017..63f3fc6 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/IdentityApiTests.cs @@ -7,6 +7,14 @@ public sealed class IdentityApiTests [Fact] public async Task AddOrUpdateRecordShouldEncodeRouteAndSendDemographicPayload() { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -18,7 +26,11 @@ public async Task AddOrUpdateRecordShouldEncodeRouteAndSendDemographicPayload() using var httpClient = new HttpClient(handler); var api = new IdentityApi( httpClient, - new IdentityApiOptions { Url = "https://identity.example.com" } + new IdentityApiOptions + { + Url = "https://identity.example.com", + ApiKey = "test-identity-api-key", + } ); await api.AddOrUpdateRecordAsync( @@ -46,6 +58,14 @@ await api.AddOrUpdateRecordAsync( [Fact] public async Task AddOrUpdateBlindedRecordShouldFlattenPayload() { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -57,7 +77,11 @@ public async Task AddOrUpdateBlindedRecordShouldFlattenPayload() using var httpClient = new HttpClient(handler); var api = new IdentityApi( httpClient, - new IdentityApiOptions { Url = "https://identity.example.com" } + new IdentityApiOptions + { + Url = "https://identity.example.com", + ApiKey = "test-identity-api-key", + } ); await api.AddOrUpdateBlindedRecordAsync( @@ -80,13 +104,25 @@ await api.AddOrUpdateBlindedRecordAsync( [Fact] public async Task DeleteRecordShouldSendEmptyObjectPayload() { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("""{"changedPersons":[]}""")) ); using var httpClient = new HttpClient(handler); var api = new IdentityApi( httpClient, - new IdentityApiOptions { Url = "https://identity.example.com" } + new IdentityApiOptions + { + Url = "https://identity.example.com", + ApiKey = "test-identity-api-key", + } ); await api.DeleteRecordAsync( @@ -103,6 +139,14 @@ await api.DeleteRecordAsync( [Fact] public async Task MonitoringShouldCallExpectedRoute() { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_IDENTITY_API_KEY"] = null, + ["ORCHESTRATE_IDENTITY_METRICS_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -114,7 +158,11 @@ public async Task MonitoringShouldCallExpectedRoute() using var httpClient = new HttpClient(handler); var api = new IdentityApi( httpClient, - new IdentityApiOptions { Url = "https://identity.example.com" } + new IdentityApiOptions + { + Url = "https://identity.example.com", + ApiKey = "test-identity-api-key", + } ); var response = await api.Monitoring.IdentifierMetricsAsync(); From c007a35e9afbfc5ed8ae878ecc7a605dad2485c8 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Wed, 1 Apr 2026 08:41:44 -0400 Subject: [PATCH 19/21] Fix Auth for Tests --- .../ApiSurfaceTests.cs | 89 ++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs index ca143cf..e840c99 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ApiSurfaceTests.cs @@ -7,16 +7,30 @@ namespace CareEvolution.Orchestrate.Tests; public sealed class ApiSurfaceTests { + private static EnvironmentVariableScope ClearAmbientOrchestrateAuthEnvironment() => + new( + new Dictionary + { + ["ORCHESTRATE_API_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); + [Fact] public async Task TerminologyBatchShouldPostToBatchRoute() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("""{"items":[{"coding":[]}]}""")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); var response = await api.Terminology.StandardizeConditionAsync([ @@ -35,13 +49,18 @@ public async Task TerminologyBatchShouldPostToBatchRoute() [Fact] public async Task ConvertHl7ShouldSendPlainTextAndQueryParameters() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("{}")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); await api.Convert.Hl7ToFhirR4Async( @@ -68,13 +87,18 @@ await api.Convert.Hl7ToFhirR4Async( [Fact] public async Task AdvancedTransportShouldApplyBaseUrlAndDeserializeJson() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); var response = await api.HttpHandler.GetJsonAsync( @@ -92,13 +116,18 @@ public async Task AdvancedTransportShouldApplyBaseUrlAndDeserializeJson() [Fact] public async Task AdvancedTransportShouldSupportTextResponses() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Text("", "text/html")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); using var content = new StringContent(""); @@ -135,13 +164,14 @@ public async Task AdvancedTransportShouldNormalizeBaseUrlAndPath( string expectedUrl ) { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("""{"message":"ok"}""")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = baseUrl } + new OrchestrateClientOptions { BaseUrl = baseUrl, ApiKey = "test-api-key" } ); _ = await api.HttpHandler.GetJsonAsync(path); @@ -180,6 +210,7 @@ public void AddOrchestrateApiShouldRegisterIOrchestrateApi() [Fact] public async Task ConvertPdfShouldReturnBytes() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var expected = new byte[] { 1, 2, 3, 4 }; var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Bytes(expected, "application/pdf")) @@ -187,7 +218,11 @@ public async Task ConvertPdfShouldReturnBytes() using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); var response = await api.Convert.CdaToPdfAsync( @@ -201,13 +236,18 @@ public async Task ConvertPdfShouldReturnBytes() [Fact] public async Task InsightRiskProfileShouldBuildExpectedQueryString() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Json("{}")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); await api.Insight.RiskProfileAsync( @@ -233,6 +273,7 @@ await api.Insight.RiskProfileAsync( [Fact] public async Task GetFhirR4CodeSystemShouldEscapePathSegment() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -242,7 +283,11 @@ public async Task GetFhirR4CodeSystemShouldEscapePathSegment() using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); _ = await api.Terminology.GetFhirR4CodeSystemAsync( @@ -272,6 +317,7 @@ public void CombinedFhirBundleFactoryShouldGenerateNdjson() [Fact] public async Task StandardizeBundleShouldSerializeAsFhirJson() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); Assert.Equal(Hl7.Fhir.Model.BundleType.BatchResponse, LiveTestData.R4Bundle.Type); Assert.NotEmpty(LiveTestData.R4Bundle.Entry); @@ -286,7 +332,11 @@ public async Task StandardizeBundleShouldSerializeAsFhirJson() using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); await api.Terminology.StandardizeBundleAsync(LiveTestData.R4Bundle); @@ -304,6 +354,7 @@ public async Task StandardizeBundleShouldSerializeAsFhirJson() [Fact] public async Task GetAllFhirR4ValueSetsForCodesShouldSerializeParametersWithValueString() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -313,7 +364,11 @@ public async Task GetAllFhirR4ValueSetsForCodesShouldSerializeParametersWithValu using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); var parameters = new Parameters @@ -349,13 +404,18 @@ public async Task GetAllFhirR4ValueSetsForCodesShouldSerializeParametersWithValu [Fact] public async Task ConvertFhirR4ToOmopShouldSerializeAsFhirJson() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Bytes([80, 75, 3, 4], "application/zip")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); _ = await api.Convert.FhirR4ToOmopAsync( @@ -375,13 +435,18 @@ public async Task ConvertFhirR4ToOmopShouldSerializeAsFhirJson() [Fact] public async Task ConvertFhirR4ToNemsisV35ShouldSerializeAsFhirJson() { + using var environment = ClearAmbientOrchestrateAuthEnvironment(); var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult(FakeResponses.Text("", "application/xml")) ); using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); _ = await api.Convert.FhirR4ToNemsisV35Async( From 115d11cf85a3919efd483bc9f1a420879cd74676 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Wed, 1 Apr 2026 09:04:20 -0400 Subject: [PATCH 20/21] Fix Auth for Tests --- .../ConfigurationTests.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs index 48c9f18..07bc1a7 100644 --- a/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs +++ b/dotnet/tests/CareEvolution.Orchestrate.Tests/ConfigurationTests.cs @@ -52,7 +52,12 @@ await api.Terminology.StandardizeConditionAsync( public async Task OrchestrateApiShouldTreatWhitespaceBaseUrlAsMissing() { using var environment = new EnvironmentVariableScope( - new Dictionary { ["ORCHESTRATE_BASE_URL"] = "https://env.example.com" } + new Dictionary + { + ["ORCHESTRATE_BASE_URL"] = "https://env.example.com", + ["ORCHESTRATE_API_KEY"] = "env-api-key", + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } ); var handler = new FakeHttpMessageHandler( @@ -242,6 +247,14 @@ public void IdentityApiShouldThrowWhenBothAuthenticationHeadersAreConfigured() [Fact] public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() { + using var environment = new EnvironmentVariableScope( + new Dictionary + { + ["ORCHESTRATE_API_KEY"] = null, + ["ORCHESTRATE_ADDITIONAL_HEADERS"] = null, + } + ); + var handler = new FakeHttpMessageHandler( (_, _) => Task.FromResult( @@ -255,7 +268,11 @@ public async Task HttpErrorsShouldBeConvertedToOrchestrateClientExceptions() using var httpClient = new HttpClient(handler); var api = new OrchestrateApi( httpClient, - new OrchestrateClientOptions { BaseUrl = "https://api.example.com" } + new OrchestrateClientOptions + { + BaseUrl = "https://api.example.com", + ApiKey = "test-api-key", + } ); var exception = await Assert.ThrowsAsync(() => From 1584bcf3bc798e82bd3a94917ddc685496d34975 Mon Sep 17 00:00:00 2001 From: Jeremy Fortune Date: Wed, 1 Apr 2026 11:00:45 -0400 Subject: [PATCH 21/21] Simplify Basic Credential Normalization See - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3022442600 - https://github.com/CareEvolution/OrchestrateSDK/pull/375#discussion_r3022458050 --- .../CareEvolution.Orchestrate/EnvironmentConfiguration.cs | 5 ++--- .../src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs | 2 +- .../src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs | 5 ----- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs index 1e3e4b8..03b91bf 100644 --- a/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs +++ b/dotnet/src/CareEvolution.Orchestrate/EnvironmentConfiguration.cs @@ -140,14 +140,13 @@ private static IReadOnlyDictionary GetAdditionalHeaders() private static string NormalizeBasicCredential(string metricsKey) { var normalized = metricsKey.Trim(); - const string prefix = "Basic"; + const string prefix = "Basic "; if ( normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && normalized.Length > prefix.Length - && char.IsWhiteSpace(normalized[prefix.Length]) ) { - normalized = normalized[(prefix.Length + 1)..].TrimStart(); + normalized = normalized[prefix.Length..].TrimStart(); } return normalized; diff --git a/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs index c4a9893..9bea51b 100644 --- a/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/IOrchestrateHttpClient.cs @@ -3,7 +3,7 @@ namespace CareEvolution.Orchestrate; [EditorBrowsable(EditorBrowsableState.Advanced)] -public interface IOrchestrateHttpClient : IDisposable +public interface IOrchestrateHttpClient { HttpClient HttpClient { get; } diff --git a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs index 976635e..6ec14e5 100644 --- a/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs +++ b/dotnet/src/CareEvolution.Orchestrate/OrchestrateHttpClient.cs @@ -479,9 +479,4 @@ private static string GetIssueDetailString(Hl7.Fhir.Model.CodeableConcept? detai return string.Empty; } - - public void Dispose() - { - _httpClient.Dispose(); - } }