Conversation
📝 WalkthroughWalkthroughAdds a patient registry and in-memory implementation, per-patient polling that emits EncounterCompletedEvent, patient CRUD endpoints, updated encounter processing to accept events and update work items (nullable ServiceRequestId/ProcedureCode), new contracts/mappers/utilities, and extensive unit and integration tests and DI wiring. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant PatientAPI as PatientEndpoints
participant WorkStore as IWorkItemStore
participant Registry as IPatientRegistry
Client->>PatientAPI: POST /api/patients/register
activate PatientAPI
PatientAPI->>WorkStore: CreateAsync(workItem Pending)
WorkStore-->>PatientAPI: workItemId
PatientAPI->>Registry: RegisterAsync(RegisteredPatient with WorkItemId)
Registry-->>PatientAPI: void
PatientAPI-->>Client: 200 OK {WorkItemId, Message}
deactivate PatientAPI
sequenceDiagram
participant Poller as AthenaPollingService
participant Registry as IPatientRegistry
participant QueryBuilder as AthenaQueryBuilder
participant Fhir as IFhirHttpClient
participant Channel as EncounterChannel
participant Processor as EncounterProcessor
participant WorkStore as IWorkItemStore
Poller->>Registry: GetActiveAsync()
Registry-->>Poller: [RegisteredPatient...]
loop per-patient (concurrent)
Poller->>QueryBuilder: BuildEncounterQuery(patient...)
QueryBuilder-->>Poller: query
Poller->>Fhir: SearchAsync(query)
Fhir-->>Poller: FHIR Bundle
alt bundle shows finished encounter
Poller->>Channel: Write(EncounterCompletedEvent)
Channel-->>Processor: EncounterCompletedEvent
Processor->>WorkStore: UpdateAsync(workItem with ServiceRequestId/ProcedureCode/status)
WorkStore-->>Processor: success
Processor->>Processor: notify WORK_ITEM_STATUS_CHANGED, PA_FORM_READY
Poller->>Registry: UnregisterAsync(patientId)
else not finished
Poller->>Registry: UpdateAsync(patientId, lastPolled, status)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/FhirDataAggregator.cs (1)
45-64:⚠️ Potential issue | 🟠 MajorThread optional
encounterIdthrough aggregator to scope ServiceRequest queries.
EncounterProcessorhas encounter context available (lines 63, 226) but cannot pass it when callingAggregateClinicalDataAsync. This causesSearchServiceRequestsAsyncto receivenull, bypassing encounter-scoped filtering and risking ServiceRequest/ProcedureCode misassociation in encounter-driven flows.Update
IFhirDataAggregator.AggregateClinicalDataAsyncandFhirDataAggregator.AggregateClinicalDataAsyncto accept an optionalencounterIdparameter and thread it through toSearchServiceRequestsAsync. The underlying FHIR client already supports this filtering (tested at line 157 inFhirClientServiceRequestTests.cs). Note:WorkItemEndpointscalls without encounter context, so the parameter must remain optional.Suggested changes
- Task<ClinicalBundle> AggregateClinicalDataAsync( - string patientId, - CancellationToken cancellationToken = default); + Task<ClinicalBundle> AggregateClinicalDataAsync( + string patientId, + string? encounterId = null, + CancellationToken cancellationToken = default);- var serviceRequestsTask = _fhirClient.SearchServiceRequestsAsync(patientId, null, cancellationToken); + var serviceRequestsTask = _fhirClient.SearchServiceRequestsAsync(patientId, encounterId, cancellationToken);Update call sites in
EncounterProcessor(lines 68–70, 231–233) to passevt.EncounterIdorencounterId.
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/DependencyExtensions.cs`:
- Around line 22-41: Replace the hard-coded origin in AddCorsPolicy by
introducing a CorsSettings POCO and binding it via
AddOptions<CorsSettings>().BindConfiguration("Cors") (same pattern used by
AddApiKeyAuthentication/AddAthenaServices/AddGatewayServices), add a "Cors"
section to appsettings.json with an Origins array (and env overrides), inject
IOptions<CorsSettings> (or resolve options in the extension) into AddCorsPolicy
to read configured origins and call policy.WithOrigins(...) with that array
before AllowAnyHeader/AllowAnyMethod/AllowCredentials; ensure null/empty
handling falls back to no origins or throws a clear configuration error.
In `@apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs`:
- Around line 23-39: The patient endpoints in MapPatientEndpoints (group with
RegisterAsync, UnregisterAsync, GetAsync) are exposed without API-key
protection; update this route group to apply the same API-key filtering used by
WorkItemEndpoints so these handlers require the API key. Specifically, locate
MapPatientEndpoints and apply the same group-level protection mechanism (the
API-key filter/authorization policy or endpoint filter used in
WorkItemEndpoints) to the MapGroup("/api/patients") so RegisterAsync,
UnregisterAsync and GetAsync inherit the API-key requirement.
In `@apps/gateway/Gateway.API/Endpoints/WorkItemEndpoints.cs`:
- Around line 91-114: The endpoint currently calls
intelligenceClient.AnalyzeAsync with workItem.ProcedureCode which can be null;
before calling AggregateClinicalDataAsync/AnalyzeAsync, validate
workItem.ProcedureCode and if missing return Results.BadRequest(new
ErrorResponse { Message = "ProcedureCode is required", Code =
"MISSING_PROCEDURE_CODE" }); also update the endpoint metadata (OpenAPI/route
documentation) to document the 400 response; adjust flow around
fhirAggregator.AggregateClinicalDataAsync, AnalyzeAsync, DetermineStatus and
workItemStore.UpdateStatusAsync so they only run when ProcedureCode is present.
- Around line 133-148: The DetermineStatus(PAFormData analysisResult) method is
doing case-sensitive comparisons against analysisResult.Recommendation which the
IntelligenceClient returns in uppercase; update the comparisons to be
case-insensitive (e.g., use string.Equals(analysisResult.Recommendation,
"APPROVE", StringComparison.OrdinalIgnoreCase) and similarly for
"NOT_REQUIRED"/"no_pa_required") or normalize analysisResult.Recommendation
(ToUpperInvariant or ToLowerInvariant) before comparing so that the checks for
WorkItemStatus.NoPaRequired and WorkItemStatus.ReadyForReview actually match the
service output; reference DetermineStatus and PAFormData.Recommendation as the
touch points.
In `@apps/gateway/Gateway.API/Services/EncounterProcessor.cs`:
- Around line 98-133: The code currently ignores the boolean results from
_workItemStore.UpdateAsync and _workItemStore.UpdateStatusAsync so notifications
may be emitted even if the update failed; modify the existingWorkItem branch
(after calling _workItemStore.UpdateAsync(evt.WorkItemId, updatedWorkItem, ct))
to capture the returned success flag, log an error and return/abort (do not
proceed to emit WORK_ITEM_STATUS_CHANGED/PA_FORM_READY) when it is false, and
likewise capture and handle the boolean result from
_workItemStore.UpdateStatusAsync in the else branch (log error and abort when
false); ensure you reference the same identifiers (_workItemStore.UpdateAsync,
UpdateStatusAsync, existingWorkItem, updatedWorkItem, evt.WorkItemId) when
adding the checks.
- Around line 89-96: The recommendation-to-status mappings are duplicated and
case-sensitive (EncounterProcessor's switch vs WorkItemEndpoints.DetermineStatus
and PAFormData docs), so create a shared utility (e.g., a static
RecommendationMapper type) that normalizes the recommendation
(recommendation?.ToUpperInvariant() or ToLowerInvariant()) and returns the
appropriate WorkItemStatus; replace the inline switch in EncounterProcessor (and
update WorkItemEndpoints.DetermineStatus to call RecommendationMapper) so all
callers use the same normalized mapping and follow the existing Strategy pattern
(similar to TokenStrategyResolver).
In `@apps/gateway/Gateway.API/Services/Polling/AthenaPollingService.cs`:
- Around line 196-255: The processed-encounter set (_processedEncounters) is
never updated in PollPatientEncounterAsync so
GetProcessedEncounterCount/IsEncounterProcessed remain empty; when you detect a
transition to finished inside PollPatientEncounterAsync (where you build evt =
new EncounterCompletedEvent and write to _encounterChannel), add the encounter
identifier (e.g., patient.PatientId + ":" + patient.EncounterId or the same key
used by IsEncounterProcessed) into _processedEncounters (ensuring thread-safety
consistent with how _processedEncounters is declared), and short-circuit early
at the start of PollPatientEncounterAsync if IsEncounterProcessed indicates this
encounter is already processed to avoid duplicate emits and keep metrics
accurate; also ensure any unregister logic still runs or is skipped as intended
after marking processed.
- Around line 38-52: Constructor in AthenaPollingService sets the encounter
channel with SingleWriter = true which conflicts with concurrent writes from
Parallel.ForEachAsync (used with MaxDegreeOfParallelism = 5); update the
Channel.CreateUnbounded<EncounterCompletedEvent> options to set SingleWriter =
false so multiple parallel writers can safely write to _encounterChannel,
ensuring the change is applied where _encounterChannel is initialized in the
AthenaPollingService constructor.
In `@docs/designs/2026-01-31-workitem-crud-preview.md`:
- Around line 144-151: The docs currently show POST
/api/work-items/{id}/rehydrate accepting a JSON body with an "accessToken",
which is incorrect; update the POST /api/work-items/{id}/rehydrate section and
the other occurrence describing the RehydrateAsync endpoint to state that the
endpoint accepts only the route parameter id and no request body, remove the
example JSON with "accessToken" and any mention of optional token/fallback
logic, and clarify that authentication/dependency resolution is handled via
service injection (RehydrateAsync) rather than a body parameter.
🟡 Minor comments (20)
docs/designs/2026-02-01-athenahealth-workflow.md-427-429 (1)
427-429:⚠️ Potential issue | 🟡 MinorAdd a language to the fenced code block.
Markdownlint flags the block as missing a language; use something like
httportext.docs/designs/2026-02-01-athenahealth-workflow.md-11-18 (1)
11-18:⚠️ Potential issue | 🟡 MinorFix table pipe spacing/alignment to satisfy MD060.
Multiple tables are missing the required space/alignment around pipes.
Also applies to: 261-268, 359-366, 374-407, 439-442
docs/integration/Current FIGMA.md-20-20 (1)
20-20:⚠️ Potential issue | 🟡 MinorUse standard horizontal rule style (
---) for markdownlint compliance.markdownlint flags the current
------style. Simple replacement keeps content unchanged.Proposed fix
------- +---Also applies to: 57-57
docs/integration/Current FIGMA.md-82-88 (1)
82-88:⚠️ Potential issue | 🟡 MinorNormalize table pipe spacing to satisfy MD060.
The tables are missing spaces around pipes in the header rows. Add spaces to align with the configured “compact” style.
Proposed fix (example for the first table)
-| Figma Assumption | Technical Reality | Impact | -|------------------|-------------------|--------| +| Figma Assumption | Technical Reality | Impact | +| ---------------- | ----------------- | ------ |Also applies to: 117-124, 129-135, 141-147, 151-158, 162-167, 171-175
docs/integration/Current FIGMA.md-92-103 (1)
92-103:⚠️ Potential issue | 🟡 MinorPromote emphasized headings to actual headings.
MD036 flags emphasis used as a heading. Consider upgrading the emphasized labels to
####headings for consistency and accessibility.Proposed fix
-**1. ADT Feed vs SMART Launch** +#### 1. ADT Feed vs SMART Launch -**2. Practice-Wide Queue vs Patient Context** +#### 2. Practice-Wide Queue vs Patient Contextdocs/pitch/proposed-miro.md-71-71 (1)
71-71:⚠️ Potential issue | 🟡 MinorIncorrect step reference — "Step 8" doesn't exist.
The loop refers to "Step 8 (Check for completeness)" but the completeness check is Phase 2, Step 4 (line 37). Consider updating to reference "Phase 2, Step 4" for clarity.
apps/gateway/Gateway.API/Configuration/AthenaOptions.cs-43-123 (1)
43-123:⚠️ Potential issue | 🟡 MinorRemove redundant OAuth scope variants to align with SMART v2 best practices.
The scope set requests
.r,.rs, and.sseparately for each resource (e.g.,Condition.r,Condition.rs,Condition.s), but.rsalready grants both read and search permissions—making the individual.rand.sscopes redundant. SMART v2 guidance explicitly recommends "preferring shorter equivalent scopes" and "factoring scopes to reduce token size," as scope string bloat impacts token size and may cause issues with stricter OAuth servers.Consolidate to only the
.rsvariant per resource (or.sif search-only is sufficient), and remove category-filtered duplicates that overlap with broader resource scopes. This reduces scope bloat, improves maintainability, and aligns with SMART v2 best practices.docs/debugging/2026-02-01-athenahealth-api-constraints.md-96-103 (1)
96-103:⚠️ Potential issue | 🟡 MinorNormalize table pipe spacing (MD060).
The tables at these lines are missing spaces around pipes. Please adjust the table formatting to satisfy MD060.📝 Example
-| Resource | Required Parameters | -|----------|-------------------| +| Resource | Required Parameters | +| -------- | ------------------- |Also applies to: 129-137, 242-249
docs/debugging/2026-02-01-athenahealth-api-constraints.md-19-21 (1)
19-21:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks (MD040).
markdownlint flagged multiple fenced blocks without languages. Please annotate them (e.g.,json,text,csharp) to satisfy MD040 and improve readability.📝 Example
-``` +```json Token request failed with status 401: {"detailedmessage":"Policy evaluation failed for this request, please check the policy configurations.","error":"access_denied"} -``` +```Also applies to: 42-44, 47-51, 56-62, 69-71, 84-91, 114-118, 121-123, 145-150, 170-176, 197-202
docs/designs/2026-01-31-workitem-crud-preview.md-40-46 (1)
40-46:⚠️ Potential issue | 🟡 MinorNormalize table spacing for markdownlint MD060.
Adding spaces around pipes resolves the “compact” table style warnings.
✏️ Suggested fix
-| Method | Path | Description | -|--------|------|-------------| +| Method | Path | Description | +| ------ | ---- | ----------- | -| **1** | Create request/response models | 3 new | 0 | +| **1** | Create request/response models | 3 new | 0 |Also applies to: 894-899
docs/designs/2026-01-31-workitem-crud-preview.md-33-36 (1)
33-36:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks (markdownlint MD040).
This clears the MD040 warnings and improves readability.
✏️ Suggested fix
-``` +```text /api/work-items -``` +``` -``` +```text Task 1 → Task 2 → Task 3 → Task 4 → Task 6 ↘ Task 5 (after Task 3) -``` +```Also applies to: 914-918
apps/gateway/Gateway.API.Tests/Endpoints/WorkItemEndpointsTests.cs-105-140 (1)
105-140:⚠️ Potential issue | 🟡 MinorClarify non‑null ProcedureCode in AnalyzeAsync stub.
WorkItem.ProcedureCodeis nullable, so the stub triggers NRT warnings. Since this test sets it, an explicit non‑null assertion keeps the intent clear.🔧 Suggested adjustment
- _intelligenceClient - .AnalyzeAsync(clinicalBundle, workItem.ProcedureCode, Arg.Any<CancellationToken>()) + _intelligenceClient + .AnalyzeAsync(clinicalBundle, workItem.ProcedureCode!, Arg.Any<CancellationToken>()) .Returns(formData);apps/gateway/Gateway.API.Tests/Endpoints/WorkItemEndpointsTests.cs-165-213 (1)
165-213:⚠️ Potential issue | 🟡 MinorClarify non‑null ProcedureCode in AnalyzeAsync stub.
Same nullable warning as above; make the expectation explicit.
🔧 Suggested adjustment
- _intelligenceClient - .AnalyzeAsync(clinicalBundle, workItem.ProcedureCode, Arg.Any<CancellationToken>()) + _intelligenceClient + .AnalyzeAsync(clinicalBundle, workItem.ProcedureCode!, Arg.Any<CancellationToken>()) .Returns(formData);docs/pitch/design-reconciliation.md-91-99 (1)
91-99:⚠️ Potential issue | 🟡 MinorFix markdownlint table alignment (MD060).
Align the pipes in the Work Item States table to satisfy the linter.
🧹 Example alignment fix
-| **State** | **Trigger** | **UI Visibility** | -| --------- | ----------- | ----------------- | -| `READY_FOR_REVIEW` | All required fields populated | Yes - "Ready for Review" queue | -| `MISSING_DATA` | Required fields incomplete | Yes - "Missing Data" queue | -| `PAYER_REQUIREMENTS_NOT_MET` | User marks as unsubmittable | Yes - "Closed" with reason | -| `SUBMITTED` | User approves, PDF written to chart | Yes - "Submitted" queue | -| `NO_PA_REQUIRED` | AI determines no PA needed for CPT/payer | No - auto-closed | +| **State** | **Trigger** | **UI Visibility** | +|:--------------------------|:-----------------------------------------|:----------------------------------| +| `READY_FOR_REVIEW` | All required fields populated | Yes - "Ready for Review" queue | +| `MISSING_DATA` | Required fields incomplete | Yes - "Missing Data" queue | +| `PAYER_REQUIREMENTS_NOT_MET` | User marks as unsubmittable | Yes - "Closed" with reason | +| `SUBMITTED` | User approves, PDF written to chart | Yes - "Submitted" queue | +| `NO_PA_REQUIRED` | AI determines no PA needed for CPT/payer | No - auto-closed |apps/gateway/Gateway.API.Tests/Services/FhirDataAggregatorTests.cs-113-119 (1)
113-119:⚠️ Potential issue | 🟡 MinorAddress nullability mismatch flagged by static analysis.
The async lambda returns
Task<PatientInfo?>but theReturnsextension expectsTask<PatientInfo>. This causes a nullability warning that should be addressed.🔧 Proposed fix
_fhirClient.GetPatientAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()) .Returns(async _ => { callOrder.Add("Patient"); await Task.Delay(10); - return CreateTestPatient(); + return CreateTestPatient()!; });Alternatively, update the mock setup to explicitly handle the nullable return:
_fhirClient.GetPatientAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()) - .Returns(async _ => + .Returns(async (_) => { callOrder.Add("Patient"); await Task.Delay(10); - return CreateTestPatient(); + return (PatientInfo?)CreateTestPatient(); });apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs-56-65 (1)
56-65:⚠️ Potential issue | 🟡 MinorStandardize WorkItem ID format.
WorkItemEndpoints prefixes IDs withwi-while registration uses a bare GUID. If consumers assume a prefix, this will be inconsistent.♻️ Align ID format with WorkItemEndpoints
- Id = Guid.NewGuid().ToString("N"), + Id = $"wi-{Guid.NewGuid():N}",docs/designs/2026-01-29-athenahealth-pivot-mvp.md-44-74 (1)
44-74:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced blocks.
markdownlint MD040 flags these;textworks well for ASCII diagrams/mockups.🧩 Example adjustment
-``` +```text ┌──────────────────────────────────────────────────────────────────────────────────┐ ... -``` +```Also applies to: 536-572, 596-627, 642-677, 692-723
docs/designs/2026-01-29-athenahealth-pivot-mvp.md-17-22 (1)
17-22:⚠️ Potential issue | 🟡 MinorNormalize table pipe spacing.
markdownlint MD060 reports inconsistent table column spacing across these sections; aligning pipes or running a formatter will clear it.Also applies to: 117-121, 421-439, 451-457, 466-487, 735-745, 752-758, 761-767, 781-789
docs/designs/2026-01-29-athenahealth-pivot-mvp.md-443-443 (1)
443-443:⚠️ Potential issue | 🟡 MinorMinor wording cleanup per LanguageTool.
Hyphenate compound adjectives and prefer “actual” over “real” in formal docs.✍️ Suggested edits
-### Rate Limiting Strategy +### Rate-Limiting Strategy-- Implement real SMART OAuth in Dashboard +- Implement actual SMART OAuth in DashboardAlso applies to: 792-797
apps/gateway/Gateway.API/Services/FhirClient.cs-439-455 (1)
439-455:⚠️ Potential issue | 🟡 MinorVerify encounter reference format with real athena payloads before implementing.
The current implementation handles bare
Encounter/{id}references, which matches all test data and current usage. However, FHIR servers can return absolute references (e.g.,https://.../Encounter/{id}) or versioned references. If athena's FHIR API sends these formats, the method will returnnulland drop the EncounterId. Before adding complexity to handle additional formats, confirm whether athena's actual payloads include anything beyond the bare format.
🧹 Nitpick comments (36)
apps/gateway/Gateway.API.Tests/Endpoints/SseEndpointsTests.cs (2)
92-93: Good improvement: polling-based subscriber wait is more deterministic.The
WaitForSubscriberAsynchelper is a solid replacement for the fixed delay—eliminates race conditions and provides a clear failure mode viaTimeoutException.However, the other tests in this file (
SseEndpoint_Get_ReturnsTextEventStreamContentType,SseEndpoint_Get_SetsCacheControlNoCache,SseEndpoint_Get_SetsConnectionKeepAlive) still useawait Task.Delay(50). Consider applying the same polling approach for consistency and reliability.♻️ Suggested refactor for consistency
// Start streaming (subscriber must be active before write) var streamTask = SseEndpoints.StreamEventsAsync(context, hub, cts.Token); - // Give subscriber time to register - await Task.Delay(50); + // Wait for subscriber to register (polling instead of fixed delay) + await WaitForSubscriberAsync(hub, TimeSpan.FromMilliseconds(200));Apply this pattern to all four tests for consistent behavior.
Also applies to: 146-159
103-103: UndisposedStreamReader.The
StreamReaderwraps theMemoryStreambut isn't disposed. While minor in test code, it's good hygiene to wrap it in ausingstatement or declaration.♻️ Proposed fix
// Assert - Check the response body memoryStream.Seek(0, SeekOrigin.Begin); - var streamedContent = new StreamReader(memoryStream).ReadToEnd(); + using var reader = new StreamReader(memoryStream); + var streamedContent = reader.ReadToEnd();docs/integration/embedded-apps.md (1)
73-73: Minor grammar and style improvements.Two small refinements for consistency:
- Line 73: "outside of" can be simplified to "outside"
- Line 148: "patient specific" should be hyphenated as "patient-specific" when used as a compound adjective
✏️ Proposed fixes
Line 73:
-- The App Dock is visible from anywhere in athenaOne; we will pass the `patientID` & `encounterID` if they are on a related page. +- The App Dock is visible from anywhere in athenaOne; we will pass the `patientID` & `encounterID` if they are on a related page. - **SMART apps** are launchable from this location, however, they will be disabled until a user navigates to a patient record. -- **Recommended use:** Admin Tools, Worklists, Patient Lists & Queues, Dashboards, Messaging & Telehealth. This launch point is ideal for universally accessible apps in athenaOne that don't require users to navigate to a specific patient. +- **Recommended use:** Admin Tools, Worklists, Patient Lists & Queues, Dashboards, Messaging & Telehealth. This launch point is ideal for universally accessible apps in athenaOne that don't require users to navigate to a specific patient.Line 148:
-- **Description:** New `patientID`, broadcast after an encounter (or any patient specific page) is finished fully loading. +- **Description:** New `patientID`, broadcast after an encounter (or any patient-specific page) is finished fully loading.Also applies to: 148-148
orchestration/AuthScript.AppHost/AppHost.cs (2)
72-77: Parameterize Athena endpoints instead of hardcoding preview URLs.Hardcoding preview endpoints makes switching to prod/stage require a code change. Prefer parameters so the environment drives these values.
♻️ Proposed change
- .WithEnvironment("Athena__FhirBaseUrl", "https://api.preview.platform.athenahealth.com/fhir/r4/") - .WithEnvironment("Athena__TokenEndpoint", "https://api.preview.platform.athenahealth.com/oauth2/v1/token") + .WithEnvironment("Athena__FhirBaseUrl", athenaFhirBaseUrl) + .WithEnvironment("Athena__TokenEndpoint", athenaTokenEndpoint)Add these parameters near the other Athena parameters:
var athenaFhirBaseUrl = builder.AddParameter("athena-fhir-base-url"); var athenaTokenEndpoint = builder.AddParameter("athena-token-endpoint");As per coding guidelines, "Focus on configuration correctness".
91-93: Gate--add-hostto Linux/Docker runtimes.
--add-hostis Docker-specific; on other runtimes or publish targets it can be unsupported or redundant. Consider applying it conditionally by OS/runtime.♻️ Proposed change
var dashboard = builder .AddDockerfile("dashboard", "../..", "apps/dashboard/Dockerfile.build") .WithHttpEndpoint(port: 3000, targetPort: 80, name: "dashboard-ui") .WaitFor(gateway) - .WithExternalHttpEndpoints() - // Enable host.docker.internal resolution on Linux - .WithContainerRuntimeArgs("--add-host=host.docker.internal:host-gateway"); + .WithExternalHttpEndpoints(); + +if (OperatingSystem.IsLinux()) +{ + // Enable host.docker.internal resolution on Linux + dashboard = dashboard.WithContainerRuntimeArgs("--add-host=host.docker.internal:host-gateway"); +}As per coding guidelines, "Focus on configuration correctness".
apps/gateway/Gateway.API.Tests/Models/RegisterPatientRequestTests.cs (1)
16-31: Consider renaming the test to reflect what's actually being verified.The test name
RegisterPatientRequest_RequiredProperties_AreEnforcedsuggests compile-time enforcement testing, but it's actually verifying property assignment and retrieval. Therequiredkeyword is enforced at compile-time, not runtime.✨ Suggested rename for clarity
[Test] -public async Task RegisterPatientRequest_RequiredProperties_AreEnforced() +public async Task RegisterPatientRequest_CanBeConstructedWithRequiredProperties() {apps/gateway/Gateway.API.Tests/Models/RegisterPatientResponseTests.cs (1)
13-26: Test name doesn't reflect full scope.The test is named
RegisterPatientResponse_ContainsWorkItemIdbut also asserts onMessage. Consider renaming toRegisterPatientResponse_Properties_InitializeCorrectlyor splitting into separate tests.✏️ Suggested rename
[Test] -public async Task RegisterPatientResponse_ContainsWorkItemId() +public async Task RegisterPatientResponse_Properties_InitializeCorrectly() {apps/gateway/Gateway.API.Tests/Services/AthenaQueryBuilderTests.cs (1)
14-15: Seal the test class to match project standards.Keeping test classes sealed aligns with the default type guidance and avoids accidental inheritance.
♻️ Suggested change
-public class AthenaQueryBuilderTests +public sealed class AthenaQueryBuilderTestsAs per coding guidelines: "Types: Sealed by default, records for DTOs, nullable enabled".
apps/gateway/Gateway.API.Tests/Services/EncounterProcessorTests.cs (2)
15-15: Seal the test class to align with the default type guidance.♻️ Suggested change
-public class EncounterProcessorTests +public sealed class EncounterProcessorTestsAs per coding guidelines: "Types: Sealed by default, records for DTOs, nullable enabled".
393-399: Avoid expression-bodied members here to match style guidance.♻️ Suggested change
- private static EncounterCompletedEvent CreateEvent() => new() - { - PatientId = "patient-1", - EncounterId = "encounter-1", - PracticeId = "practice-1", - WorkItemId = "workitem-1" - }; + private static EncounterCompletedEvent CreateEvent() + { + return new EncounterCompletedEvent + { + PatientId = "patient-1", + EncounterId = "encounter-1", + PracticeId = "practice-1", + WorkItemId = "workitem-1" + }; + }As per coding guidelines: "Control: Guard clauses first, early return, no arrow code".
apps/gateway/Gateway.API.Tests/Models/WorkItemStatusTests.cs (1)
11-11: Seal the test class to match the default type guidance.♻️ Suggested change
-public class WorkItemStatusTests +public sealed class WorkItemStatusTestsAs per coding guidelines: "Types: Sealed by default, records for DTOs, nullable enabled".
apps/gateway/Gateway.API.Tests/Models/CreateWorkItemRequestTests.cs (2)
7-24: Test name doesn't match what's being tested.The method
CreateWorkItemRequest_RequiredProperties_InitializesCorrectlyalso testsServiceRequestIdandProcedureCode, which according to the AI summary are optional properties. Consider renaming toCreateWorkItemRequest_AllProperties_InitializesCorrectlyor splitting into separate tests for required vs optional properties.
26-40: Duplicate setup with first test.This test duplicates the object initialization from the first test. Consider using a shared fixture or
[SetUp]method, or consolidating this assertion into the first test since both verify the same object's behavior.apps/gateway/Gateway.API/Services/FhirTokenProvider.cs (2)
20-24: Add constructor guard clauses.Per coding guidelines, guard clauses should come first. The constructor should validate its dependencies are not null.
🛡️ Proposed fix
public FhirTokenProvider(ITokenStrategyResolver resolver, ILogger<FhirTokenProvider> logger) { - _resolver = resolver; - _logger = logger; + _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); }
27-39: Consider logging success at Debug level.The error path is logged, but successful token acquisition isn't. A debug-level log on success can aid troubleshooting without being noisy in production.
📝 Optional enhancement
public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default) { var strategy = _resolver.Resolve(); var token = await strategy.AcquireTokenAsync(cancellationToken); if (token is null) { _logger.LogError("Failed to acquire FHIR access token"); throw new InvalidOperationException("Unable to acquire FHIR access token"); } + _logger.LogDebug("Successfully acquired FHIR access token"); return token; }apps/gateway/Gateway.API/Configuration/ApiKeySettings.cs (1)
13-16: ConsiderIReadOnlyList<string>for immutability.For security-sensitive configuration like API keys, using
IReadOnlyList<string>prevents accidental mutation after configuration binding.🔒 Proposed change
/// <summary> /// List of valid API keys. /// </summary> - public List<string> ValidApiKeys { get; init; } = []; + public IReadOnlyList<string> ValidApiKeys { get; init; } = [];apps/gateway/Gateway.API.Tests/DependencyExtensionsTests.cs (1)
24-25: Consider disposing theServiceProviderafter use.
BuildServiceProvider()returns anIDisposable. While not critical in short-lived tests, disposing it ensures any registeredIDisposableservices are properly cleaned up.♻️ Suggested fix using `using` statement
services.AddGatewayServices(); - var provider = services.BuildServiceProvider(); - var workItemStore = provider.GetService<IWorkItemStore>(); + using var provider = services.BuildServiceProvider(); + var workItemStore = provider.GetService<IWorkItemStore>();Apply the same pattern in
AddGatewayServices_RegistersPatientRegistry.Also applies to: 43-44
apps/gateway/Gateway.API.Tests/Contracts/IFhirClientTests.cs (1)
28-29: Nullable parameter type not verified by reflection.The interface declares
encounterIdasstring?(nullable), buttypeof(string)andtypeof(string?)are identical at runtime. This assertion passes but doesn't validate the nullability annotation. Consider checkingNullabilityInfoContextif nullability verification is important, or acknowledge this limitation.apps/gateway/Gateway.API.Tests/Contracts/IWorkItemStoreTests.cs (1)
18-32: Add UpdateAsync/signature assertions to strengthen the contract test.
Right now the test only validates a subset of methods and doesn’t guard against signature drift. Consider assertingUpdateAsync(and optionally parameter/return types) to keep the interface contract tight.🔧 Suggested addition
// Verify UpdateStatusAsync method var updateMethod = interfaceType.GetMethod("UpdateStatusAsync"); await Assert.That(updateMethod).IsNotNull(); + // Verify UpdateAsync method + var updateAsyncMethod = interfaceType.GetMethod("UpdateAsync"); + await Assert.That(updateAsyncMethod).IsNotNull();apps/gateway/Gateway.API.Tests/Services/FhirClientServiceRequestTests.cs (2)
12-13: Seal the test class to match the default-sealed convention.✅ Suggested change
-public class FhirClientServiceRequestTests +public sealed class FhirClientServiceRequestTestsAs per coding guidelines: Sealed by default.
74-76: Dispose JsonDocument instances to avoid test-time leaks.
JsonDocumentisIDisposable. Considerusing var(or a shared helper) so documents are released after each test.🧹 Example for one test (apply similarly elsewhere)
- var jsonDocument = JsonDocument.Parse(fhirBundle); + using var jsonDocument = JsonDocument.Parse(fhirBundle);Also applies to: 121-123, 152-154, 207-209, 247-249, 288-290
apps/gateway/Gateway.API/Filters/ApiKeySecuritySchemeTransformer.cs (1)
19-32: Consider merging into existing SecuritySchemes for defensive robustness.The current code directly assigns
document.Components.SecuritySchemes = securitySchemes;, which works safely in the current architecture since this is the only transformer populating security schemes. However, for future-proofing, merging into the existing dictionary follows a more defensive pattern:document.Components ??= new OpenApiComponents(); - document.Components.SecuritySchemes = securitySchemes; + document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>(); + document.Components.SecuritySchemes[SecuritySchemeName] = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Name = "X-API-Key", + In = ParameterLocation.Header, + Description = "API key required for protected endpoints" + };This eliminates the redundant intermediate dictionary and ensures compatibility if other scheme sources are added later.
apps/gateway/Gateway.API.Tests/Models/RegisteredPatientTests.cs (1)
14-15: Consider sealing this test class for consistency.This follows the project’s “sealed by default” standard and keeps intent explicit.
As per coding guidelines: “
apps/gateway/**/*.cs: Types: Sealed by default”.apps/gateway/Gateway.API.Tests/Services/InMemoryPatientRegistryTests.cs (1)
15-17: Consider sealing this test class for consistency.Aligns with the “sealed by default” standard.
As per coding guidelines: “
apps/gateway/**/*.cs: Types: Sealed by default”.apps/gateway/Gateway.API.Tests/Models/WorkItemTests.cs (1)
5-6: Consider sealing this test class for consistency.Aligns with the “sealed by default” standard.
As per coding guidelines: “
apps/gateway/**/*.cs: Types: Sealed by default”.apps/gateway/Gateway.API.Tests/Integration/PatientEndpointsIntegrationTests.cs (1)
32-32: Avoid expression-bodied helpers to match the guideline.🔧 Suggested change
- private void AddApiKey(Scenario s) => s.WithRequestHeader(ApiKeyHeader, GatewayAlbaBootstrap.TestApiKey); + private void AddApiKey(Scenario s) + { + s.WithRequestHeader(ApiKeyHeader, GatewayAlbaBootstrap.TestApiKey); + }As per coding guidelines: Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Contracts/IPatientRegistryTests.cs (1)
9-9: Seal the test class to align with the guideline.🔧 Suggested change
-public class IPatientRegistryTests +public sealed class IPatientRegistryTestsAs per coding guidelines: Types: Sealed by default, records for DTOs, nullable enabled.
apps/gateway/Gateway.API.Tests/Models/WorkItemListResponseTests.cs (1)
5-5: Seal the test class to align with the guideline.🔧 Suggested change
-public class WorkItemListResponseTests +public sealed class WorkItemListResponseTestsAs per coding guidelines: Types: Sealed by default, records for DTOs, nullable enabled.
apps/gateway/Gateway.API/Filters/ApiKeyEndpointFilter.cs (1)
17-20: Add a null guard for the injected validator.🔧 Suggested change
- public ApiKeyEndpointFilter(IApiKeyValidator validator) - { - _validator = validator; - } + public ApiKeyEndpointFilter(IApiKeyValidator validator) + { + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + }As per coding guidelines: Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Integration/WorkItemEndpointsIntegrationTests.cs (1)
92-122: Status filter test relies on test isolation.The filter test creates data and immediately queries for it. This works with
PerTestSessionshared state. Be aware that test execution order could affect results if other tests createSubmitteditems.Consider using a more unique identifier pattern (e.g.,
$"enc-filter-test-{Guid.NewGuid()}") if test isolation becomes an issue, though for current scope this is fine.apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs (1)
167-178: Unused helper methodCreateEmptyBundle.This method is defined but not referenced in any test. Consider removing it or adding a test that uses it.
♻️ Remove if unused
- private static JsonElement CreateEmptyBundle() - { - var json = """ - { - "resourceType": "Bundle", - "type": "searchset", - "total": 0, - "entry": [] - } - """; - return JsonDocument.Parse(json).RootElement; - }apps/gateway/Gateway.API.Tests/Services/FhirTokenProviderTests.cs (1)
32-55: Consider using a more idiomatic exception assertion.The manual try-catch pattern works but is verbose. If TUnit supports
Assert.ThrowsAsync, that would be more concise.♻️ Alternative using ThrowsAsync (if supported)
[Test] public async Task GetTokenAsync_WhenStrategyReturnsNull_ThrowsInvalidOperationException() { // Arrange _resolver.Resolve().Returns(_strategy); _strategy.AcquireTokenAsync(Arg.Any<CancellationToken>()).Returns((string?)null); var provider = new FhirTokenProvider(_resolver, NullLogger<FhirTokenProvider>.Instance); // Act & Assert - InvalidOperationException? exception = null; - try - { - await provider.GetTokenAsync(); - } - catch (InvalidOperationException ex) - { - exception = ex; - } - - // Assert - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("FHIR access token"); + var exception = await Assert.ThrowsAsync<InvalidOperationException>( + async () => await provider.GetTokenAsync()); + await Assert.That(exception.Message).Contains("FHIR access token"); }apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs (1)
96-103: Simplify the idempotent delete signature.
This method always returns Ok, so the NotFound alternative in the return type is unused.🔧 Simplify return type
- public static async Task<Results<Ok, NotFound>> UnregisterAsync( + public static async Task<Ok> UnregisterAsync( string patientId, [FromServices] IPatientRegistry patientRegistry, CancellationToken ct = default) { await patientRegistry.UnregisterAsync(patientId, ct).ConfigureAwait(false); return TypedResults.Ok(); }apps/gateway/Gateway.API/Services/Polling/AthenaPollingService.cs (1)
263-282: Use Firely SDK to parse and validate the FHIR Bundle.
The current code assumesentry[0]is an Encounter without validating the resource type. SinceHl7.Fhir.R4is already available, useFhirJsonParser.Parse<Bundle>()to parse the JSON and validate each entry before extracting the status. This ensures type safety and catches structural issues early, per coding guidelines.apps/gateway/Gateway.API.Tests/Services/InMemoryWorkItemStoreTests.cs (2)
9-16: Seal the test class by default.
Keeps the type non-inheritable and aligns with the “sealed by default” guideline. Please confirm your test framework supports sealed test classes.🛠️ Suggested change
-public class InMemoryWorkItemStoreTests +public sealed class InMemoryWorkItemStoreTestsAs per coding guidelines: Types: Sealed by default.
309-338: Assert the exact UpdatedAt value.
Only checking non-null could miss regressions where UpdatedAt is overwritten. Consider asserting equality with the expected timestamp and verify your assertion library supports this check.✅ Suggested tightening
- await Assert.That(retrieved!.UpdatedAt).IsNotNull(); + await Assert.That(retrieved!.UpdatedAt).IsEqualTo(updateTime);
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add interface for managing registered patients awaiting encounter completion. Methods include RegisterAsync, GetActiveAsync, UnregisterAsync, GetAsync, and UpdateAsync for tracking patient registration state during encounter polling workflows. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement the in-memory patient registry for development and testing. Uses ConcurrentDictionary for thread-safe storage keyed by PatientId. Implements RegisterAsync and GetAsync methods; other methods stubbed. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add GetActiveAsync implementation that returns all active (non-expired) patients from the in-memory registry. Patients are considered expired if their RegisteredAt timestamp is older than 12 hours. Also adds test coverage for GetAsync returning null for non-existent patients. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…istry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These fields are populated after analysis, not at registration time. Updated both WorkItem and CreateWorkItemRequest models with updated XML documentation explaining when these fields are populated. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a static utility class that properly formats athenahealth-specific
FHIR query parameters, including the required ah-practice parameter
with the correct Organization/a-1.Practice-{practiceId} format.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new Pending status (value 0) to WorkItemStatus enum for representing work items where the patient is registered but awaiting encounter completion. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add IPatientRegistry as a constructor dependency to AthenaPollingService, preparing for the refactor from global polling to per-patient polling. The registry dependency is stored but not yet used - actual per-patient polling logic will be added in a subsequent change. - Add _patientRegistry field to store the dependency - Update constructor to accept IPatientRegistry parameter - Add new test for constructor with patient registry - Update all existing tests to include the new parameter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactored ExecuteAsync in AthenaPollingService to poll registered patients using Parallel.ForEachAsync instead of the previous global polling approach. The service now: - Gets active patients from IPatientRegistry before polling - Returns early if no patients are registered (no FHIR calls made) - Polls each registered patient in parallel (max 5 concurrent) - Per-patient FHIR query logic to be implemented in Task 016 Updated tests to reflect new per-patient polling model: - Added tests verifying registry is queried - Added test verifying FHIR client not called when no patients - Marked encounter processing tests as skipped (Task 016) - Removed obsolete global polling tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add event model emitted when a monitored encounter transitions to "finished" status. The event is published to a channel and consumed by the EncounterProcessor. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add request body model for patient registration API endpoint with required properties: PatientId, EncounterId, and PracticeId. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add response model for patient registration API endpoint containing WorkItemId and Message properties. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ction Add per-patient polling that queries FHIR for encounter status and detects when encounters transition to "finished". When completed: - Emits encounter ID to channel for downstream processing - Auto-unregisters patient from active polling - Updates registry with LastPolledAt and CurrentEncounterStatus Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add test to verify the edge case where a provider registers a patient whose encounter is ALREADY finished. The existing condition (status == "finished" && patient.CurrentEncounterStatus != "finished") already handles this correctly because null != "finished" is true in C#. The test confirms that when CurrentEncounterStatus is null (first poll) and the FHIR status is "finished", the event is emitted immediately and the patient is unregistered. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update the encounter polling channel from Channel<string> to Channel<EncounterCompletedEvent> to pass full context (PatientId, EncounterId, PracticeId, WorkItemId) to the processor. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…d update work item status Add new ProcessAsync method that accepts EncounterCompletedEvent with full context (PatientId, EncounterId, PracticeId, WorkItemId) and updates work item status based on analysis recommendation: - APPROVE/DENY -> ReadyForReview - NEEDS_INFO -> MissingData - NOT_REQUIRED -> NoPaRequired Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…vice request info - Add UpdateAsync method to IWorkItemStore interface for updating work items with arbitrary fields - Implement UpdateAsync in InMemoryWorkItemStore with retry logic for concurrent modifications - Update EncounterProcessor.ProcessAsync to set ServiceRequestId from ClinicalBundle and ProcedureCode from PAFormData after analysis - Add comprehensive tests for UpdateAsync and work item field updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Send a WORK_ITEM_STATUS_CHANGED notification via SSE when the EncounterProcessor updates the work item status. The notification includes WorkItemId, NewStatus, PatientId, ServiceRequestId (if available), and ProcedureCode (if available). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add integration tests verifying the full HTTP flow for patient registration endpoints using Alba test host. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add integration tests that verify the end-to-end encounter processing flow: - EncounterCompletion_UpdatesWorkItemStatus: Verifies status changes from Pending to ReadyForReview - EncounterCompletion_WorkItemHasProcedureCode: Verifies procedure code is set after processing - EncounterCompletion_WorkItemHasServiceRequestId: Verifies ServiceRequestId is extracted from clinical bundle - EncounterCompletion_WorkItemHasUpdatedAt: Verifies UpdatedAt timestamp is set Includes a dedicated Alba bootstrap (EncounterProcessingAlbaBootstrap) that: - Mocks FHIR services with clinical bundle containing service requests - Provides null IConnectionMultiplexer to skip Redis storage in tests - Inherits API key and Athena configuration patterns from existing bootstrap Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7ab03c7 to
23062ab
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API.Tests/Services/EncounterProcessorTests.cs`:
- Around line 458-540: The tests
ProcessAsync_AnalysisApprove_UpdatesWorkItemToReadyForReview,
ProcessAsync_AnalysisDeny_UpdatesWorkItemToReadyForReview,
ProcessAsync_AnalysisNeedsInfo_UpdatesWorkItemToMissingData, and
ProcessAsync_AnalysisNotRequired_UpdatesWorkItemToNoPaRequired are currently
hitting the fallback path because GetByIdAsync on _workItemStore is not mocked;
either mock _workItemStore.GetByIdAsync to return a valid WorkItem (so the
primary flow is exercised) or, if testing the fallback is intentional, rename
the tests to indicate the "WorkItemNotFound" fallback case; update the test
setup in each test (or SetupSuccessfulMocks) to call
_workItemStore.GetByIdAsync(...).Returns(Task.FromResult(workItem)) when
validating primary UpdateStatusAsync calls, or rename the test methods to
include "_WorkItemNotFound_FallsBack" to reflect the current behavior.
- Around line 430-456: The test currently triggers the fallback path because
_workItemStore.GetByIdAsync is not mocked and returns null; update the Arrange
section to mock _workItemStore.GetByIdAsync to return an existing WorkItem
(matching evt.WorkItemId) so the primary path in _sut.ProcessAsync executes and
calls UpdateAsync; then change the Assert to verify
_workItemStore.Received(1).UpdateAsync(...) was called with the expected updated
WorkItem (instead of asserting UpdateStatusAsync), keeping references to
ProcessAsync, _workItemStore.GetByIdAsync, _workItemStore.UpdateAsync, and the
work item id "workitem-abc".
In `@apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs`:
- Around line 49-87: RegisterAsync currently creates a WorkItem and
RegisteredPatient without validating request fields; add a guard clause at the
start of RegisterAsync to validate request.PatientId, request.EncounterId, and
request.PracticeId using string.IsNullOrWhiteSpace (or equivalent) and return a
BadRequest<string> (e.g., TypedResults.BadRequest) with a clear message naming
the missing/invalid field(s) if any validation fails; ensure this check occurs
before constructing WorkItem or calling workItemStore.CreateAsync and
patientRegistry.RegisterAsync so only valid inputs proceed.
🧹 Nitpick comments (9)
apps/gateway/Gateway.API.Tests/Services/AthenaQueryBuilderTests.cs (1)
14-14: Consider sealing the test class.Per coding guidelines, types should be sealed by default. Test classes are rarely inherited.
-public class AthenaQueryBuilderTests +public sealed class AthenaQueryBuilderTestsapps/gateway/Gateway.API.Tests/Contracts/IPatientRegistryTests.cs (1)
33-128: Remove parameter name assertions from signature tests.While the project currently has no trimming/AOT enabled, parameter names aren't part of the public contract and asserting them couples tests to implementation details. Stick to type and arity assertions for stable, resilient tests.
♻️ Suggested change (apply similarly to other signature tests)
- await Assert.That(parameters[0].Name).IsEqualTo("patient"); await Assert.That(parameters[0].ParameterType).IsEqualTo(typeof(RegisteredPatient)); - await Assert.That(parameters[1].Name).IsEqualTo("ct"); await Assert.That(parameters[1].ParameterType).IsEqualTo(typeof(CancellationToken));apps/gateway/Gateway.API/Services/InMemoryWorkItemStore.cs (1)
59-81: Retry loop may be ineffective under contention.Unlike
UpdateStatusAsync(which recomputes the update inside the loop), this method uses the caller-providedupdatedon every retry. IfTryUpdatefails due to concurrent modification, subsequent retries will likely fail too because:
- The caller's
updatedwas derived from a snapshot taken before calling this method- On retry,
existingis fresh butupdatedremains stale and won't matchFor an MVP in-memory store with low contention this is acceptable, but the retry loop gives a false sense of resilience. Consider either:
- Documenting that this performs a simple compare-and-swap (single attempt is semantically equivalent)
- Accepting
Func<WorkItem, WorkItem>to allow recomputation on retry (likeUpdateStatusAsyncdoes internally)♻️ Alternative: Accept a transform function for true retry support
- public Task<bool> UpdateAsync(string id, WorkItem updated, CancellationToken cancellationToken = default) + public Task<bool> UpdateAsync(string id, Func<WorkItem, WorkItem> transform, CancellationToken cancellationToken = default) { const int maxRetries = 10; for (var attempt = 0; attempt < maxRetries; attempt++) { if (!_store.TryGetValue(id, out var existing)) { return Task.FromResult(false); } + var updated = transform(existing); if (_store.TryUpdate(id, updated, existing)) { return Task.FromResult(true); } } return Task.FromResult(false); }apps/gateway/Gateway.API.Tests/Services/InMemoryPatientRegistryTests.cs (1)
17-17: Shared registry instance may cause flaky tests.The
_registryfield is initialized once and shared across all test methods. SinceInMemoryPatientRegistrymaintains state, tests likeGetActiveAsync_ReturnsAllActivePatients(expecting exactly 2 patients) may fail depending on execution order—patients registered by other tests could still be active.Consider recreating the registry per test via
[SetUp]or using a local variable in each test method.♻️ Proposed fix for test isolation
public class InMemoryPatientRegistryTests { - private readonly InMemoryPatientRegistry _registry = new(); + private InMemoryPatientRegistry _registry = null!; + + [SetUp] + public void SetUp() + { + _registry = new InMemoryPatientRegistry(); + }apps/gateway/Gateway.API/Services/InMemoryPatientRegistry.cs (2)
36-44: Expired entries accumulate in memory.
GetActiveAsyncfilters out expired patients but doesn't remove them from the dictionary. For a dev/test implementation this may be acceptable, but long-running instances will accumulate stale entries.Consider adding periodic cleanup or lazy eviction:
♻️ Optional: Add cleanup during GetActiveAsync
public Task<IReadOnlyList<RegisteredPatient>> GetActiveAsync(CancellationToken ct = default) { var cutoff = DateTimeOffset.UtcNow - ExpirationTime; - var active = _patients.Values - .Where(p => p.RegisteredAt > cutoff) - .ToList(); + var expired = _patients.Where(kvp => kvp.Value.RegisteredAt <= cutoff).Select(kvp => kvp.Key).ToList(); + foreach (var key in expired) + { + _patients.TryRemove(key, out _); + } + var active = _patients.Values.ToList(); return Task.FromResult<IReadOnlyList<RegisteredPatient>>(active); }
53-69: Race condition between read and update.The
TryGetValuefollowed byTryUpdatecan fail if another thread modifies the entry between these calls. While returningfalseis safe, callers may not expect this behavior during normal operation.Consider a retry loop similar to
InMemoryWorkItemStore.UpdateAsync(per the relevant snippet showing max 10 retries):♻️ Suggested retry pattern
public Task<bool> UpdateAsync(string patientId, DateTimeOffset lastPolled, string status, CancellationToken ct = default) { - if (!_patients.TryGetValue(patientId, out var existing)) + const int maxRetries = 10; + for (var attempt = 0; attempt < maxRetries; attempt++) { - return Task.FromResult(false); - } + if (!_patients.TryGetValue(patientId, out var existing)) + { + return Task.FromResult(false); + } - var updated = existing with - { - LastPolledAt = lastPolled, - CurrentEncounterStatus = status - }; + var updated = existing with + { + LastPolledAt = lastPolled, + CurrentEncounterStatus = status + }; - var success = _patients.TryUpdate(patientId, updated, existing); - return Task.FromResult(success); + if (_patients.TryUpdate(patientId, updated, existing)) + { + return Task.FromResult(true); + } + } + return Task.FromResult(false); }apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs (1)
78-106: Timing-based test may be flaky in CI.The test relies on
Task.Delay(180)with a 1-second polling interval to verify "at least once" behavior. Under CI load, timing can vary causing intermittent failures.Consider using a
TaskCompletionSourceor similar synchronization primitive instead of fixed delays for more deterministic tests.apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs (1)
96-103: Return type declaresNotFoundbut it's never returned.The signature
Results<Ok, NotFound>suggests the endpoint may return 404, but the implementation always returnsOk(). Either simplify the return type to justOk, or returnNotFoundwhen the patient wasn't actually registered (ifUnregisterAsyncshould be non-idempotent).♻️ Suggested fix for idempotent semantics (current behavior)
- public static async Task<Results<Ok, NotFound>> UnregisterAsync( + public static async Task<Ok> UnregisterAsync( string patientId, [FromServices] IPatientRegistry patientRegistry, CancellationToken ct = default) { await patientRegistry.UnregisterAsync(patientId, ct).ConfigureAwait(false); return TypedResults.Ok(); }apps/gateway/Gateway.API.Tests/Services/InMemoryWorkItemStoreTests.cs (1)
256-338: Consider adding a concurrent update test.The
InMemoryWorkItemStore.UpdateAsyncimplementation includes retry logic (up to 10 retries withTryUpdate) to handle concurrent modifications. A test exercising concurrent updates would validate this behavior and prevent regressions in the thread-safety guarantees.
Fixes: - P3 Major: 9 issues addressed Changes: - Add API-key filter to PatientEndpoints for PHI protection - Add ProcedureCode guard in rehydrate endpoint (returns 400 if missing) - Create RecommendationMapper utility for case-insensitive recommendation matching - Handle UpdateAsync/UpdateStatusAsync failures before emitting notifications - Fix SingleWriter=false for concurrent channel writes in AthenaPollingService - Update processed-encounter tracking with early return and mark on completion - Update docs to remove outdated accessToken references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/designs/2026-01-31-workitem-crud-preview.md (1)
559-593:⚠️ Potential issue | 🟡 MinorTest examples still reference obsolete
RehydrateRequestwithAccessToken.The endpoint documentation at lines 144-146 states the rehydrate endpoint takes no request body, but the test examples here still construct a
RehydrateRequestwithWorkItemIdandAccessTokenproperties and POST it as a JSON body. Update these test examples to align with the documented behavior (POST to the URL with no body).
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API.Tests/Services/EncounterProcessorTests.cs`:
- Around line 430-456: The test currently triggers the fallback path because
_workItemStore.GetByIdAsync isn't mocked; update the test so it exercises the
primary path by mocking _workItemStore.GetByIdAsync to return a WorkItem
instance and then assert that UpdateAsync (not just UpdateStatusAsync) on
_workItemStore is called after calling _sut.ProcessAsync(evt, ...);
alternatively, if you intend to test the fallback behavior, rename the test to
ProcessAsync_WorkItemNotFound_UpdatesStatusViaFallback and keep the current
assertions that UpdateStatusAsync("workitem-abc", WorkItemStatus.ReadyForReview,
...) is invoked.
🧹 Nitpick comments (2)
apps/gateway/Gateway.API/Endpoints/WorkItemEndpoints.cs (1)
51-56: Add.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)metadata for the rehydrate endpoint.The endpoint can now return 400 when
ProcedureCodeis missing (lines 92-101), but the endpoint metadata only declares 200 and 404 responses. Adding the 400 response improves OpenAPI documentation accuracy.📝 Suggested metadata update
// REHYDRATE group.MapPost("/{id}/rehydrate", RehydrateAsync) .WithName("RehydrateWorkItem") .WithSummary("Re-fetch clinical data and re-analyze work item") .Produces<RehydrateResponse>(StatusCodes.Status200OK) - .Produces<ErrorResponse>(StatusCodes.Status404NotFound); + .Produces<ErrorResponse>(StatusCodes.Status404NotFound) + .Produces<ErrorResponse>(StatusCodes.Status400BadRequest);apps/gateway/Gateway.API/Services/Polling/AthenaPollingService.cs (1)
284-303: Simple status extraction helper.The
ExtractEncounterStatusmethod correctly navigates the FHIR bundle structure with null-safe property access. Consider adding a brief XML doc comment for consistency with the rest of the file.
|
@coderabbitai Full review. |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Endpoints/WorkItemEndpoints.cs (1)
51-56:⚠️ Potential issue | 🟡 MinorAdd 400 response to endpoint metadata.
RehydrateAsync returns BadRequest on line 96 when ProcedureCode is missing, but the endpoint only declares 200 and 404 responses. Add.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)to keep the OpenAPI contract complete.Proposed update
group.MapPost("/{id}/rehydrate", RehydrateAsync) .WithName("RehydrateWorkItem") .WithSummary("Re-fetch clinical data and re-analyze work item") .Produces<RehydrateResponse>(StatusCodes.Status200OK) + .Produces<ErrorResponse>(StatusCodes.Status400BadRequest) .Produces<ErrorResponse>(StatusCodes.Status404NotFound);
🤖 Fix all issues with AI agents
In
`@apps/gateway/Gateway.API.Tests/Integration/EncounterProcessingAlbaBootstrap.cs`:
- Around line 87-90: The test bootstrap registers a nullable generic type which
prevents resolving IConnectionMultiplexer and triggers static-analysis warnings;
update the registration after services.RemoveAll<IConnectionMultiplexer>() to
register the non-nullable service type (IConnectionMultiplexer) and supply a
null-forgiving value (null!) so DI keys match and warnings stop — change the
call to use services.AddSingleton<IConnectionMultiplexer>(...) (keeping the
factory or direct null-forgiving value) in EncounterProcessingAlbaBootstrap.cs.
In `@apps/gateway/Gateway.API.Tests/Services/InMemoryPatientRegistryTests.cs`:
- Around line 81-95: The test GetActiveAsync_ReturnsAllActivePatients assumes a
clean registry; ensure test isolation by resetting or recreating the registry
before arranging: either call a clear/reset method on _registry (e.g.,
_registry.ClearAsync()/Reset()) or instantiate a fresh InMemoryPatientRegistry
for this test so prior registrations don't affect results; keep using
CreatePatient and RegisterAsync as-is and then assert that
_registry.GetActiveAsync() returns exactly the two patients registered.
- Around line 15-17: The test class InMemoryPatientRegistryTests uses a shared
field _registry that preserves state across tests causing isolation failures
(e.g., GetActiveAsync_ReturnsAllActivePatients fails if
RegisterAsync_SamePatientTwice_OverwritesPrevious ran earlier); fix by
reinitializing the registry before each test — replace the readonly field usage
with a per-test setup that assigns _registry = new InMemoryPatientRegistry() in
a method annotated with the test framework's setup attribute (use the same
Before(Test) pattern used in AthenaPollingServiceTests.cs) so each test gets a
fresh instance.
In
`@apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs`:
- Around line 79-105: The test method
ExecuteAsync_RespectsPollingInterval_CallsRegistryMultipleTimes is misleading
because the polling loop in PollForFinishedEncountersAsync() invokes a poll
immediately on StartAsync(_sut) and then waits the interval, so with the current
1s interval and ~180ms wait only one poll occurs; rename the test method to
something like ExecuteAsync_StartsPollingImmediately or
ExecuteAsync_CallsRegistryAtLeastOnce and update any references to that method
name to reflect the new intent (keep assertions and Arrange/Act/Assert
unchanged, referencing _sut.StartAsync, _patientRegistry.GetActiveAsync, and
_sut.StopAsync).
In `@apps/gateway/Gateway.API/Services/InMemoryPatientRegistry.cs`:
- Around line 54-68: UpdateAsync can return false spuriously under concurrent
updates because a single TryUpdate may fail; implement a retry loop similar to
InMemoryWorkItemStore.UpdateStatusAsync: repeatedly read the current entry from
_patients (use TryGetValue or indexer), build the updated record (as done with
existing with { ... }), and attempt _patients.TryUpdate(patientId, updated,
current) until TryUpdate succeeds or the patient no longer exists or the
CancellationToken is canceled; ensure you return true on success and false if
the patient disappears or cancellation occurs, and avoid infinite loops (respect
ct or include a reasonable retry limit).
In `@apps/gateway/Gateway.API/Services/Polling/AthenaPollingService.cs`:
- Around line 244-267: The code currently marks the encounter as processed
(writes to _processedEncounters inside lock) before calling
_encounterChannel.Writer.WriteAsync, which can silently drop the event if
WriteAsync fails; move the processed-marking to after WriteAsync completes
successfully: call await _encounterChannel.Writer.WriteAsync(evt,
ct).ConfigureAwait(false) first, then inside the lock set
_processedEncounters[patient.EncounterId] = DateTimeOffset.UtcNow; keep the
LogInformation and the subsequent await
_patientRegistry.UnregisterAsync(patient.PatientId, ct).ConfigureAwait(false)
unchanged so that failures from WriteAsync leave the encounter unmarked and
eligible for retry.
🧹 Nitpick comments (13)
apps/gateway/Gateway.API/Services/AthenaQueryBuilder.cs (1)
29-30: Add guard clauses + encode query values, and use a block body.This prevents invalid requests when inputs are empty and avoids malformed query strings if values contain reserved characters.
♻️ Proposed change
- public static string BuildEncounterQuery(string patientId, string encounterId, string practiceId) - => $"patient={patientId}&_id={encounterId}&ah-practice=Organization/a-1.Practice-{practiceId}"; + public static string BuildEncounterQuery(string patientId, string encounterId, string practiceId) + { + if (string.IsNullOrWhiteSpace(patientId)) + { + throw new ArgumentException("Patient ID is required.", nameof(patientId)); + } + + if (string.IsNullOrWhiteSpace(encounterId)) + { + throw new ArgumentException("Encounter ID is required.", nameof(encounterId)); + } + + if (string.IsNullOrWhiteSpace(practiceId)) + { + throw new ArgumentException("Practice ID is required.", nameof(practiceId)); + } + + var encodedPatientId = Uri.EscapeDataString(patientId); + var encodedEncounterId = Uri.EscapeDataString(encounterId); + var encodedPracticeId = Uri.EscapeDataString(practiceId); + + return $"patient={encodedPatientId}&_id={encodedEncounterId}&ah-practice=Organization/a-1.Practice-{encodedPracticeId}"; + }As per coding guidelines: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Services/AthenaQueryBuilderTests.cs (1)
14-14: Seal the test class by default.♻️ Proposed change
-public class AthenaQueryBuilderTests +public sealed class AthenaQueryBuilderTestsAs per coding guidelines: Types sealed by default.
apps/gateway/Gateway.API/Contracts/IWorkItemStore.cs (1)
35-42: Good addition for full work item updates.The method complements
UpdateStatusAsyncwell for scenarios requiring atomic updates to multiple fields.One consideration: the contract doesn't specify whether
updated.Idmust match theidparameter. While the current usage (viawithexpressions) preserves the Id, consider adding a note to the XML doc or enforcing this invariant in implementations to prevent accidental misuse.📝 Optional: Clarify id consistency in documentation
/// <summary> /// Updates a work item with new values. /// </summary> /// <param name="id">The work item ID.</param> - /// <param name="updated">The updated work item.</param> + /// <param name="updated">The updated work item. Must have the same Id as the <paramref name="id"/> parameter.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>True if update succeeded, false if work item not found.</returns>apps/gateway/Gateway.API/Services/InMemoryWorkItemStore.cs (1)
59-81: Correct retry pattern, but consider validating id consistency.The implementation correctly mirrors
UpdateStatusAsync's atomic retry pattern. However,TryUpdate(id, updated, existing)stores theupdateditem at keyidwithout verifying thatupdated.Idmatchesid. If they differ, the stored work item'sIdproperty won't match its dictionary key, leading to inconsistent state.While current callers use
existingWorkItem with {...}(preserving the Id), adding a guard clause would make this defensive:🛡️ Proposed defensive validation
public Task<bool> UpdateAsync(string id, WorkItem updated, CancellationToken cancellationToken = default) { + if (updated.Id != id) + { + throw new ArgumentException($"Updated work item Id '{updated.Id}' must match the provided id '{id}'", nameof(updated)); + } + const int maxRetries = 10; for (var attempt = 0; attempt < maxRetries; attempt++)apps/gateway/Gateway.API/Endpoints/WorkItemEndpoints.cs (1)
145-146: Avoid expression-bodied members here to align with “no arrow code”.
Prefer a block-bodied method for DetermineStatus to match the project’s C# style rules.Style-aligned refactor
-private static WorkItemStatus DetermineStatus(PAFormData analysisResult) => - RecommendationMapper.MapToStatus(analysisResult.Recommendation, analysisResult.ConfidenceScore); +private static WorkItemStatus DetermineStatus(PAFormData analysisResult) +{ + return RecommendationMapper.MapToStatus(analysisResult.Recommendation, analysisResult.ConfidenceScore); +}As per coding guidelines, Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Models/RegisterPatientResponseTests.cs (1)
13-26: Test provides minimal value for a record type.This test only verifies that record properties retain their assigned values, which is guaranteed by the compiler for
required initproperties. Consider either removing this trivial test or expanding it to cover serialization/deserialization behavior if that's a concern for the API response.apps/gateway/Gateway.API.Tests/Endpoints/PatientEndpointsTests.cs (1)
73-86: Minor:.Returns(Task.CompletedTask)is redundant.NSubstitute returns
Task.CompletedTaskby default for methods returningTask. This line can be removed without affecting test behavior.♻️ Simplification
RegisteredPatient? capturedPatient = null; - _patientRegistry.RegisterAsync(Arg.Do<RegisteredPatient>(p => capturedPatient = p), Arg.Any<CancellationToken>()) - .Returns(Task.CompletedTask); + _patientRegistry.RegisterAsync(Arg.Do<RegisteredPatient>(p => capturedPatient = p), Arg.Any<CancellationToken>());apps/gateway/Gateway.API/Services/InMemoryPatientRegistry.cs (1)
23-26: Add guard clauses for invalid registration input.
Avoid null/empty patient IDs to prevent NREs and invalid dictionary keys.✅ Suggested guard clauses
public Task RegisterAsync(RegisteredPatient patient, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(patient); + if (string.IsNullOrWhiteSpace(patient.PatientId)) + { + throw new ArgumentException("PatientId is required.", nameof(patient)); + } _patients[patient.PatientId] = patient; return Task.CompletedTask; }As per coding guidelines: Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Integration/EncounterProcessingIntegrationTests.cs (1)
34-34: Prefer block-bodied helper (no arrow code guideline).🔧 Suggested change
- private void AddApiKey(Scenario s) => s.WithRequestHeader(ApiKeyHeader, EncounterProcessingAlbaBootstrap.TestApiKey); + private void AddApiKey(Scenario s) + { + s.WithRequestHeader(ApiKeyHeader, EncounterProcessingAlbaBootstrap.TestApiKey); + }As per coding guidelines: Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Services/EncounterProcessorTests.cs (1)
393-399: Convert expression-bodied CreateEvent to block form.🔧 Suggested change
- private static EncounterCompletedEvent CreateEvent() => new() - { - PatientId = "patient-1", - EncounterId = "encounter-1", - PracticeId = "practice-1", - WorkItemId = "workitem-1" - }; + private static EncounterCompletedEvent CreateEvent() + { + return new EncounterCompletedEvent + { + PatientId = "patient-1", + EncounterId = "encounter-1", + PracticeId = "practice-1", + WorkItemId = "workitem-1" + }; + }As per coding guidelines: Control: Guard clauses first, early return, no arrow code.
apps/gateway/Gateway.API.Tests/Models/EncounterCompletedEventTests.cs (1)
13-30: Test provides limited value but serves as documentation.This test essentially verifies that record properties can be initialized and read—something the compiler guarantees. Consider expanding to test edge cases (e.g., equality semantics,
withexpressions) if the model evolves, or removing if test coverage budget is tight.apps/gateway/Gateway.API/Services/EncounterProcessor.cs (1)
235-333: Consider removing the obsolete method or extracting shared logic.The
[Obsolete]annotation guides callers away, but ~100 lines of duplicate code remain. If removal is imminent, that's fine. If this will linger, consider extracting the common hydration/analysis/PDF-generation steps into a private helper to reduce maintenance burden.apps/gateway/Gateway.API/Endpoints/PatientEndpoints.cs (1)
98-105: Return type includesNotFoundbut method always returnsOk.The signature declares
Results<Ok, NotFound>, but the implementation always returnsOk(). If this is intentional for idempotency (documented in the XML comment), simplify the return type to justOk. Otherwise, if you plan to returnNotFoundwhen the patient doesn't exist, the logic needs updating.♻️ Simplify return type if idempotent behavior is intentional
- public static async Task<Results<Ok, NotFound>> UnregisterAsync( + public static async Task<Ok> UnregisterAsync( string patientId, [FromServices] IPatientRegistry patientRegistry, CancellationToken ct = default) { await patientRegistry.UnregisterAsync(patientId, ct).ConfigureAwait(false); return TypedResults.Ok(); }
apps/gateway/Gateway.API.Tests/Integration/EncounterProcessingAlbaBootstrap.cs
Show resolved
Hide resolved
apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs
Show resolved
Hide resolved
Major: - Thread encounterId through FhirDataAggregator to scope ServiceRequest queries Minor: - Simplify UnregisterAsync return type from Results<Ok, NotFound> to Ok - Convert AddApiKey helper to block form - Convert CreateEvent helper to block form Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/Services/EncounterProcessor.cs`:
- Around line 79-108: AnalyzeAsync is always called with DefaultProcedureCode
causing incorrect analysis for non-default procedures; before calling
_intelligenceClient.AnalyzeAsync derive the procedure code from the encounter
bundle by selecting the first active ServiceRequest's procedure code (e.g.,
clinicalBundle.ServiceRequests.FirstOrDefault(sr => sr.Status ==
"active")?.Code) and fall back to DefaultProcedureCode if missing, then pass
that derivedCode into AnalyzeAsync; keep extracting ServiceRequestId the same
and ensure updatedWorkItem.ProcedureCode reflects the procedure code returned by
AnalyzeAsync (or the derived code if AnalyzeAsync doesn't override it).
🧹 Nitpick comments (1)
apps/gateway/Gateway.API.Tests/Services/EncounterProcessorTests.cs (1)
15-16: Consider sealing the test fixture to match project conventions.
This class doesn’t appear to be designed for inheritance; marking itsealedaligns with the “sealed by default” rule.🔧 Suggested change
-public class EncounterProcessorTests +public sealed class EncounterProcessorTestsAs per coding guidelines: Types: Sealed by default, records for DTOs, nullable enabled.
- Seal AthenaQueryBuilderTests class - Add id validation to InMemoryWorkItemStore.UpdateAsync - Convert DetermineStatus to block form - Remove redundant Returns(Task.CompletedTask) - Add guard clauses to InMemoryPatientRegistry.RegisterAsync Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract procedure code from the first active ServiceRequest in the clinical bundle, falling back to DefaultProcedureCode when not available. This ensures correct analysis for non-default procedures. Addresses: #20 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@coderabbitai If there are no remaining unaddressed critical/major issues, approve this PR. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API.Tests/Endpoints/PatientEndpointsTests.cs`:
- Around line 73-74: The test's substitute setup uses Arg.Do<RegisteredPatient>
to capture the argument but doesn't return an awaited Task, risking race
conditions; update the _patientRegistry.RegisterAsync setup (the call using
Arg.Do<RegisteredPatient> and capturedPatient) to return a completed Task (e.g.,
append .Returns(Task.CompletedTask)) so the mocked RegisterAsync is awaitable
and the capturedPatient is reliably set before assertions.
In `@apps/gateway/Gateway.API/Services/EncounterProcessor.cs`:
- Around line 79-170: The PA analysis can return a null/empty
formData.ProcedureCode which currently overwrites the existing
WorkItem.ProcedureCode and sends null in the notification; change the update and
notification to fall back to the derived procedureCode variable when
formData.ProcedureCode is null or empty. Specifically, in EncounterProcessor.cs
where AnalyzeAsync returns formData, set the updatedWorkItem.ProcedureCode =
string.IsNullOrEmpty(formData.ProcedureCode) ? procedureCode :
formData.ProcedureCode and likewise set the Notification's ProcedureCode to the
same fallback value (use the local procedureCode and formData variables to
compute the finalProcedureCode before calling _workItemStore.UpdateAsync and
creating the statusChangeNotification).
|
✅ Strong Architecture
✅ Thread Safety
✅ Memory Management
✅ Comprehensive Testing
✅ Error Handling
✅ API Design
No critical or major issues found. The code is production-ready with excellent test coverage and follows .NET best practices. ✅ Actions performedComments resolved and changes approved. |
Summary
Implements patient registry infrastructure and per-patient encounter polling for the athenahealth SMART on FHIR embedded app architecture. This enables dashboard-driven patient registration with automatic encounter monitoring and work item lifecycle management.
Changes
IPatientRegistryinterface withInMemoryPatientRegistryimplementation for tracking registered patients with 12-hour expirationPOST/GET/DELETE /api/patients/*for patient registration and management with automatic work item creation in Pending statusAthenaPollingServicefrom global polling to per-patient polling usingParallel.ForEachAsyncwith max 5 concurrentPollPatientEncounterAsyncdetects encounter completion and emitsEncounterCompletedEventwith auto-unregistrationEncounterCompletedEvent, update work items with ServiceRequestId/ProcedureCode, and sendWORK_ITEM_STATUS_CHANGEDSSE notificationsTest Plan
Unit and integration tests covering all new functionality with TDD approach.
Results: Tests 291 ✓ · Build 0 errors
Design: docs/designs/2026-02-01-athenahealth-workflow.md
Plan: docs/plans/2026-02-01-patient-registry-polling.md