Skip to content

Conversation

thomasyopes
Copy link
Contributor

@thomasyopes thomasyopes commented Aug 19, 2025

Issues:

Description

Testing

Check each PR.

Release Plan

  • ⚠️ Points to master
  • Merge this

Summary by CodeRabbit

  • New Features

    • Centralized flow to create medical document references and generate upload URLs.
    • Athena integration: optional pruning of blacklisted Encounter types and automatic Encounter summary uploads (configurable per customer).
    • Added ability to fetch EHR documents and obtain pre-signed document URLs.
    • New bundle variant to represent removed data contributions.
    • Healthie added to default EHR sources.
    • Expanded recognition for breaths-per-minute units.
  • Documentation

    • Upload examples updated to show PDF uploads using file bytes and Content-Type headers.
  • Chores

    • Developer Docker image now builds the FHIR SDK.

Thomas Yopes added 30 commits August 11, 2025 11:05
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-559

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…-athena-contribution-update

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…-athena-contribution-update

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…source

Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…-athena-contribution-update

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…-athena-contribution-update

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…-athena-contribution-update

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Ref: ENG-634

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
…pdate

feat(ehr): sollis contribute ehr summaries + remove bad encounters
Copy link

coderabbitai bot commented Aug 19, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Centralizes medical document upload into a new orchestrator that upserts DocumentReference and returns a presigned S3 URL. Adds Athena-specific encounter preprocessing (appointment-type blacklist, optional summaries upload), new S3-backed document fetch utilities, bundle type for removed contributions, schema additions, and minor renames/refactors.

Changes

Cohort / File(s) Summary of changes
Docs: document upload examples
docs/medical-api/api-reference/document/post-upload-url.mdx
Example updated for PDF uploads: payload key changed to fileContent, headers now include Content-Type: application/pdf and removed Content-Length.
Dev image build: include fhir-sdk
packages/api/Dockerfile.dev
Adds RUN npm run build -w packages/fhir-sdk to the dev Docker build steps.
API: document upload orchestration + routes
packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts, packages/api/src/routes/medical/document.ts
New exported getUploadUrlAndCreateDocRef orchestrates creating/upserting a DocumentReference and generating an S3 presigned PUT URL (runs FHIR upsert and S3 pre-signed URL generation in parallel); routes now delegate to a shared wrapper and remove inline S3/FHIR wiring.
API: default EHR sources
packages/api/src/command/mapping/patient.ts
Adds EhrSources.healthie to defaultSources.
EHR contribution pipeline: Athena-specific handling
packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
Adds Athena mapping usage, computes practiceId, prunes Encounters by appointment-type blacklist (and any resources referencing them), optionally uploads encounter HTML summaries, and produces a "contribution removed" bundle for removed resources.
Athena fetch: conditional flags from mappings
packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts, .../get-resource-bundle-by-resource-id.ts
For Encounter resources fetches secondary mappings and conditionally sets attachAppointmentType and fetchEncounterSummary flags on downstream calls.
Athena SDK: appointment type and summaries
packages/core/src/external/ehr/athenahealth/index.ts
Adds helper to read encounter appointment-type extension, optionally attaches appointment-type extension to Encounters, optionally fetches and creates/stores encounter HTML summaries, and exposes attachAppointmentType/fetchEncounterSummary flags on public methods. Also moves encounterAppointmentExtensionUrl to private and adds getEncounterAppointmentTypeIdExtension.
Bundle types/keys: contribution removed
packages/core/src/external/ehr/bundle/bundle-shared.ts
Adds RESOURCE_DIFF_DATA_CONTRIBUTION_REMOVED enum member, new file-key generator createFileKeyResourceDiffDataContributionRemoved, updates isResourceDiffBundleType and createKeyMap.
Bundle fetch: bucket parameterization
packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
Adds s3BucketName parameter to fetchBundle and fetchBundlePreSignedUrl, improves error messages and attaches bucket context to errors.
Document I/O utilities: fetch
packages/core/src/external/ehr/document/command/fetch-document.ts
New module exposing fetchDocument and fetchDocumentPreSignedUrl to read document content and generate pre-signed URLs from S3 by document type, with optional lastModified and detailed error/context handling.
Document write: style-only
packages/core/src/external/ehr/document/command/create-or-replace-document.ts
Minor formatting: added braces around an existing throw; no functional change.
FHIR shared helper rename
packages/core/src/external/fhir/shared/index.ts, .../references.ts
Renames buildEntryReferencebuildResourceReference and updates call sites.
Units: respiratory rate synonyms
packages/core/src/external/ehr/unit-conversion.ts
isBpm now recognizes "breaths/min" and "breaths per minute" in addition to existing synonyms.
Schema: Athena secondary mappings
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
Adds optional contributionEncounterAppointmentTypesBlacklist: string[] and contributionEncounterSummariesEnabled: boolean to athenaSecondaryMappingsSchema (and inferred AthenaSecondaryMappings type).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API Route (/medical/document)
  participant Orchestrator (getUploadUrlAndCreateDocRef)
  participant FHIR Server
  participant S3

  Client->>API Route: POST /upload-url (docRef draft)
  API Route->>Orchestrator: cxId, patientId, docRefDraft
  par Parallel ops
    Orchestrator->>FHIR Server: Upsert DocumentReference
    Orchestrator->>S3: Generate presigned PUT URL (key from doc path)
  end
  Orchestrator-->>API Route: { documentReferenceId, uploadUrl }
  API Route-->>Client: { documentReferenceId, uploadUrl }

  note over Client,S3: Client then PUTs file bytes to uploadUrl (application/pdf)
Loading
sequenceDiagram
  autonumber
  participant Caller
  participant ContributeDiffBundle
  participant MappingsStore
  participant AthenaSDK
  participant S3 Docs

  Caller->>ContributeDiffBundle: contributeResourceDiffBundle(bundle)
  ContributeDiffBundle->>MappingsStore: getSecondaryMappings(ehr=athena, practiceId)
  MappingsStore-->>ContributeDiffBundle: mappings
  alt Blacklist present
    ContributeDiffBundle->>ContributeDiffBundle: remove Encounters with blacklisted appointment types
    ContributeDiffBundle->>ContributeDiffBundle: remove resources referencing removed Encounters
    ContributeDiffBundle->>S3 Docs: create "contribution removed" bundle
  end
  ContributeDiffBundle->>AthenaSDK: fetch bundle (attachAppointmentType?, fetchEncounterSummary?)
  AthenaSDK-->>ContributeDiffBundle: Bundle (with optional appointment types/summaries)
  opt Summaries enabled
    loop each Encounter needing summary
      ContributeDiffBundle->>S3 Docs: fetchDocument(existing?)
      alt Missing
        ContributeDiffBundle->>Orchestrator: getUploadUrlAndCreateDocRef
        Orchestrator-->>ContributeDiffBundle: uploadUrl, docRefId
        ContributeDiffBundle->>S3 Docs: PUT HTML summary via uploadUrl
      end
    end
  end
  ContributeDiffBundle-->>Caller: completed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd0b8d6 and c969e2a.

📒 Files selected for processing (1)
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (3 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/api/Dockerfile.dev (1)

12-18: Add @metriport/fhir-sdk build to all production Dockerfiles & align package deps

Our scan shows only packages/api/Dockerfile.dev builds packages/fhir-sdk. Production Dockerfiles and some downstream packages import the SDK but don’t build it or declare it as a dependency:

• packages/api/Dockerfile
– Missing RUN npm run build -w packages/fhir-sdk before building the API.
• Other service Dockerfiles (data-transformation/fhir-to-csv, fhir-converter, mllp-server, terminology)
– None include a build for packages/fhir-sdk; add the same step if their code (directly or via packages/utils/packages/core) consumes FHIR SDK.
• packages/utils/package.json
– Imports @metriport/fhir-sdk in source but doesn’t declare it; add it to dependencies.
• packages/core/package.json
– Correctly declares a file:packages/fhir-sdk dependency—ensure its build order remains after fhir-sdk.

Please update the Dockerfiles and package.json files above so that every consumer builds the FHIR SDK first and declares it as a dependency.

packages/core/src/external/ehr/bundle/bundle-shared.ts (1)

93-100: Type guard and union type are inconsistent; fix the narrowing

isResourceDiffBundleType returns true for RESOURCE_DIFF_DATA_CONTRIBUTION_REMOVED and RESOURCE_DIFF_WRITE_BACK, but ResourceDiffBundleType excludes both. This makes the type guard unsound and can cause incorrect narrowing downstream.

Recommend expanding the union to match the guard (or restrict the guard). Suggest expanding.

Apply this diff to align the union with the guard:

 export type ResourceDiffBundleType =
   | BundleType.RESOURCE_DIFF_EHR_ONLY
   | BundleType.RESOURCE_DIFF_METRIPORT_ONLY
-  | BundleType.RESOURCE_DIFF_DATA_CONTRIBUTION;
+  | BundleType.RESOURCE_DIFF_DATA_CONTRIBUTION
+  | BundleType.RESOURCE_DIFF_DATA_CONTRIBUTION_REMOVED
+  | BundleType.RESOURCE_DIFF_WRITE_BACK;
🧹 Nitpick comments (17)
packages/api/Dockerfile.dev (1)

13-13: Optional: consolidate SDK builds to reduce duplication across Dockerfiles

If you maintain similar steps in multiple Dockerfiles, consider a single root NPM script (e.g., build:sdks) that builds shared + fhir-sdk + other SDKs in the correct order, and call that once here. It keeps Dockerfiles DRY and reduces drift.

packages/core/src/external/ehr/unit-conversion.ts (1)

206-209: Clarify semantics: mixing breath-rate units into isBpm may conflate HR vs RR; consider explicit support.

If loincCodeMap maps respiratory rate targets to "bpm", this is fine, but it risks mislabeling RR as bpm. Either:

  • keep the union intentionally and document it, or
  • add a dedicated check (e.g., isRespPerMin) and map RR targets to a neutral unit like "1/min".

Also consider additional common synonyms to improve coverage.

Would you like to include more RR synonyms? Minimal addition shown below.

   return (
     units === "bpm" ||
     units === "/min" ||
     units === "beats/min" ||
     units === "per minute" ||
     units === "br/min" ||
     units === "breaths/min" ||
-    units === "breaths per minute"
+    units === "breaths per minute" ||
+    units === "resp/min" ||
+    units === "respirations/min"
   );
packages/core/src/external/fhir/shared/index.ts (1)

307-309: Rename is fine; add a deprecated alias to avoid cross-package breakages.

If any downstream package still imports buildEntryReference, a temporary alias smooths the transition.

Apply this alias just below the new function:

 export function buildResourceReference(resource: Resource): string {
   return `${resource.resourceType}/${resource.id}`;
 }
+
+/** @deprecated Use buildResourceReference */
+export const buildEntryReference = buildResourceReference;
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (1)

10-12: Schema additions match intended optional controls

  • contributionEncounterAppointmentTypesBlacklist: optional string[]
  • contributionEncounterSummariesEnabled: optional boolean

These will zod-validate presence/shape and are compatible with “presence implies enablement” semantics downstream.

Consider adding short comments documenting semantics:

  • Blacklist: presence enables appointment-type tracking; entries define types to exclude.
  • Summaries: true enables encounter summary upload path.
packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (2)

285-331: Summary upload pre-check is sound; minor nit on “predecessor” ID source

  • Skips upload if an “upload” job artifact already exists (good).
  • Falls back to fetching the “latest” source summary.

Confirm that getPredecessorExtensionValue returning valueRelatedArtifact.display is indeed the canonical encounter identifier expected by your document keying. If the ID should be taken from a Reference or from valueRelatedArtifact.resource, adjust accordingly.

If display is free text in your extension, prefer an explicit identifier field to avoid mismatches.


335-384: Upload flow is correct; ensure headers match presigned policy

Using axios.put with explicit Content-Length and Content-Type is standard. Be aware that some S3 policies require exact matching headers; if presigned URLs include additional constraints (e.g., x-amz-acl), propagate them here.

If needed, extend getUploadUrlAndCreateDocRef to return required headers and pass them through.

packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts (2)

41-46: Avoid enabling attachAppointmentType when the blacklist is empty

An empty array is truthy in JS/TS, so this condition will enable attachAppointmentType even when the blacklist is empty. If the intent is to enable only when there are entries, check for length.

Apply this minimal change:

-    ...(mappings?.contributionEncounterAppointmentTypesBlacklist && {
+    ...(mappings?.contributionEncounterAppointmentTypesBlacklist?.length && {
       attachAppointmentType: true,
     }),

21-33: Minor perf: fetch client and mappings in parallel

Client creation and mappings retrieval are independent. You can shave off latency by doing them concurrently.

Apply this refactor:

-  const client = await createAthenaHealthClient({
-    cxId,
-    practiceId,
-    ...(tokenId && { tokenId }),
-  });
-  const mappings =
-    resourceType === "Encounter"
-      ? await getSecondaryMappings({
-          ehr: EhrSources.athena,
-          practiceId,
-          schema: athenaSecondaryMappingsSchema,
-        })
-      : undefined;
+  const [client, mappings] = await Promise.all([
+    createAthenaHealthClient({
+      cxId,
+      practiceId,
+      ...(tokenId && { tokenId }),
+    }),
+    resourceType === "Encounter"
+      ? getSecondaryMappings({
+          ehr: EhrSources.athena,
+          practiceId,
+          schema: athenaSecondaryMappingsSchema,
+        })
+      : Promise.resolve(undefined),
+  ]);
packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts (2)

39-44: Avoid enabling attachAppointmentType when the blacklist is empty

Align with the intent: only set attachAppointmentType if the blacklist has entries.

Apply this minimal change:

-    ...(mappings?.contributionEncounterAppointmentTypesBlacklist && {
+    ...(mappings?.contributionEncounterAppointmentTypesBlacklist?.length && {
       attachAppointmentType: true,
     }),

25-33: DRY opportunity: extract common Encounter options logic

This Encounter-specific mappings fetch + flag computation is duplicated in both commands. Consider a small helper (e.g., getAthenaEncounterRequestOptions(practiceId)) returning { attachAppointmentType?: true; fetchEncounterSummary?: true } to reduce duplication and ensure parity between the two endpoints.

packages/api/src/routes/medical/document.ts (1)

232-241: Consider pushing validation into the shared command to prevent misuse

To ensure future non-route callers don’t bypass validation, you could move docRefCheck into the command function (or add an optional validate flag defaulting to true). This keeps invariants centralized.

If you want to centralize validation, apply the following changes in the command module (see the next file comment for concrete diffs).

packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (2)

7-7: Centralize draft validation in the command

Import docRefCheck alongside composeDocumentReference so the command can enforce input validity regardless of caller. This prevents accidental misuse outside the route.

Apply this import tweak:

-import { composeDocumentReference } from "../../../external/fhir/document/draft-update-document-reference";
+import { composeDocumentReference, docRefCheck } from "../../../external/fhir/document/draft-update-document-reference";

26-31: Validate draft and enrich logging with docRefId

Add docRefCheck(docRefDraft) to enforce invariants and include docRefId in the log for better traceability.

Apply this change:

-  const { log } = out(`getUploadUrlAndCreateDocRef - patient ${patientId}`);
+  const { log } = out(`getUploadUrlAndCreateDocRef - patient ${patientId} docRefId ${docRefId}`);
+  docRefCheck(docRefDraft);
packages/core/src/external/ehr/athenahealth/index.ts (4)

1567-1576: Avoid unnecessary work when both flags are false.

We always call dangerouslyAdjustEncountersInBundle for Encounter resources even if both attachAppointmentType and fetchEncounterSummary are false. Short-circuit to skip the call and the iteration for performance.

Apply this diff:

-          if (resourceType === "Encounter") {
+          if (resourceType === "Encounter" && (attachAppointmentType || fetchEncounterSummary)) {
             await client.dangerouslyAdjustEncountersInBundle({
               cxId,
               metriportPatientId,
               athenaPatientId,
               bundle: validBundle,
               attachAppointmentType,
               fetchEncounterSummary,
             });
           }

1668-1677: Repeat the guard here to skip unnecessary adjustment.

Same optimization as the other path.

Apply this diff:

-          if (resourceType === "Encounter") {
+          if (resourceType === "Encounter" && (attachAppointmentType || fetchEncounterSummary)) {
             await client.dangerouslyAdjustEncountersInBundle({
               cxId,
               metriportPatientId,
               athenaPatientId,
               bundle: validBundle,
               attachAppointmentType,
               fetchEncounterSummary,
             });
           }

1948-1954: Plumb jobId through the params so it can be used downstream.

The type for the params includes jobId, but it isn’t destructured, so it’s unavailable inside the function. If we intend to tag S3 documents with jobId (recommended), destructure it here.

Apply this diff:

   private async dangerouslyAdjustEncountersInBundle({
     cxId,
     metriportPatientId,
     athenaPatientId,
     bundle,
+    jobId,
     attachAppointmentType = false,
     fetchEncounterSummary = false,
   }: {
     cxId: string;
     metriportPatientId: string;
     athenaPatientId: string;
     jobId?: string | undefined;
     bundle: EhrStrictFhirResourceBundle;
     attachAppointmentType?: boolean;
     fetchEncounterSummary?: boolean;
   }): Promise<void> {

Also applies to: 1960-1962


2002-2035: Propagate jobId to document fetch/store for better traceability.

Pass jobId to fetchDocument and createOrReplaceDocument so generated S3 keys (and logs) can be traced to the running job. This also keeps parity with other document commands.

Apply this diff:

-              const existingEncounterSummary = await fetchDocument({
+              const existingEncounterSummary = await fetchDocument({
                 ehr: EhrSources.athena,
                 cxId,
                 metriportPatientId,
                 ehrPatientId: athenaPatientId,
                 documentType: DocumentType.HTML,
                 resourceType: "Encounter",
                 resourceId: encounterId,
+                jobId,
               });
@@
-              await createOrReplaceDocument({
+              await createOrReplaceDocument({
                 ehr: EhrSources.athena,
                 cxId,
                 metriportPatientId,
                 ehrPatientId: athenaPatientId,
                 documentType: DocumentType.HTML,
                 payload: encounterSummary,
                 resourceType: "Encounter",
                 resourceId: encounterId,
+                jobId,
               });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7de92d5 and cd0b8d6.

📒 Files selected for processing (17)
  • docs/medical-api/api-reference/document/post-upload-url.mdx (1 hunks)
  • packages/api/Dockerfile.dev (1 hunks)
  • packages/api/src/command/mapping/patient.ts (1 hunks)
  • packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (1 hunks)
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (3 hunks)
  • packages/api/src/routes/medical/document.ts (4 hunks)
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts (2 hunks)
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts (2 hunks)
  • packages/core/src/external/ehr/athenahealth/index.ts (9 hunks)
  • packages/core/src/external/ehr/bundle/bundle-shared.ts (4 hunks)
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts (3 hunks)
  • packages/core/src/external/ehr/document/command/create-or-replace-document.ts (1 hunks)
  • packages/core/src/external/ehr/document/command/fetch-document.ts (1 hunks)
  • packages/core/src/external/ehr/unit-conversion.ts (1 hunks)
  • packages/core/src/external/fhir/shared/index.ts (1 hunks)
  • packages/core/src/external/fhir/shared/references.ts (4 hunks)
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Use const whenever possible
Use async/await instead of .then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, like notEnabled, prefer isDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use /** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible

Files:

  • packages/core/src/external/ehr/document/command/fetch-document.ts
  • packages/core/src/external/ehr/unit-conversion.ts
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/routes/medical/document.ts
  • packages/api/src/command/mapping/patient.ts
  • packages/core/src/external/ehr/document/command/create-or-replace-document.ts
  • packages/core/src/external/fhir/shared/index.ts
  • packages/core/src/external/fhir/shared/references.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/document/command/fetch-document.ts
  • packages/core/src/external/ehr/unit-conversion.ts
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/routes/medical/document.ts
  • packages/api/src/command/mapping/patient.ts
  • packages/core/src/external/ehr/document/command/create-or-replace-document.ts
  • packages/core/src/external/fhir/shared/index.ts
  • packages/core/src/external/fhir/shared/references.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
**/*.ts

⚙️ CodeRabbit Configuration File

**/*.ts: - Use the Onion Pattern to organize a package's code in layers

  • Try to use immutable code and avoid sharing state across different functions, objects, and systems
  • Try to build code that's idempotent whenever possible
  • Prefer functional programming style functions: small, deterministic, 1 input, 1 output
  • Minimize coupling / dependencies
  • Avoid modifying objects received as parameter
  • Only add comments to code to explain why something was done, not how it works
  • Naming
    • classes, enums: PascalCase
    • constants, variables, functions: camelCase
    • file names: kebab-case
    • table and column names: snake_case
    • Use meaningful names, so whoever is reading the code understands what it means
    • Don’t use negative names, like notEnabled, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any to other types
    • Type predicates: only applicable to narrow down the type, not to force a complete type conversion
    • Prefer deconstructing parameters for functions instead of multiple parameters that might be of
      the same type
    • Don’t use null inside the app, only on code interacting with external interfaces/services,
      like DB and HTTP; convert to undefined before sending inwards into the code
    • Use async/await instead of .then()
    • Use the strict equality operator ===, don’t use abstract equality operator ==
    • When calling a Promise-returning function asynchronously (i.e., not awaiting), use .catch() to
      handle errors (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
  • Avoid creating arrow functions
  • U...

Files:

  • packages/core/src/external/ehr/document/command/fetch-document.ts
  • packages/core/src/external/ehr/unit-conversion.ts
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/routes/medical/document.ts
  • packages/api/src/command/mapping/patient.ts
  • packages/core/src/external/ehr/document/command/create-or-replace-document.ts
  • packages/core/src/external/fhir/shared/index.ts
  • packages/core/src/external/fhir/shared/references.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
🧠 Learnings (27)
📓 Common learnings
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts:221-233
Timestamp: 2025-08-14T20:43:26.281Z
Learning: In Athena EHR integration, encounter references are housed specifically in the 'encounter' and 'context' fields as checked by the doesResourceReferToEncounter function in packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts. No broadening of encounter reference detection is needed since the current implementation matches Athena's exact data structure.
📚 Learning: 2025-04-21T17:07:30.574Z
Learnt from: thomasyopes
PR: metriport/metriport#3608
File: packages/core/src/external/ehr/canvas/index.ts:451-469
Timestamp: 2025-04-21T17:07:30.574Z
Learning: The `getMetriportOnlyBundleByResourceType` method in CanvasApi returns `Promise<Bundle | undefined>` because the Metriport-only bundle is a computed artifact that may not exist yet. In contrast, `getBundleByResourceType` returns `Promise<Bundle>` because it can always fetch from the EHR API if the S3 cached bundle doesn't exist.

Applied to files:

  • packages/core/src/external/ehr/document/command/fetch-document.ts
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-08-17T18:46:42.110Z
Learnt from: leite08
PR: metriport/metriport#4399
File: packages/scripts/deploy-api.sh:29-30
Timestamp: 2025-08-17T18:46:42.110Z
Learning: In the Metriport codebase, `/dist` directories for SDK packages (like `packages/fhir-sdk/dist`) are generated at build time and not committed to the repository. Deployment scripts correctly reference these build artifacts that exist during the CI/CD pipeline packaging step.

Applied to files:

  • packages/api/Dockerfile.dev
📚 Learning: 2025-03-19T13:58:17.253Z
Learnt from: leite08
PR: metriport/metriport#3463
File: packages/api-sdk/src/medical/models/patient.ts:1-1
Timestamp: 2025-03-19T13:58:17.253Z
Learning: When changes are made to SDK packages (`api-sdk`, `commonwell-sdk`, `carequality-sdk`) or `packages/shared` in the Metriport codebase, alpha versions need to be published to NPM before merging the PR.

Applied to files:

  • packages/api/Dockerfile.dev
📚 Learning: 2025-08-13T21:37:33.680Z
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts:25-29
Timestamp: 2025-08-13T21:37:33.680Z
Learning: In Athena EHR integration (packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts), thomasyopes prefers hard-failing when secondary mappings fetch fails rather than graceful degradation, because these mappings control critical behaviors like contributionEncounterAppointmentTypesBlacklist and contributionEncounterSummariesEnabled. Running with default values when mappings are unavailable could lead to incorrect processing.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-08-14T20:43:26.281Z
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts:221-233
Timestamp: 2025-08-14T20:43:26.281Z
Learning: In Athena EHR integration, encounter references are housed specifically in the 'encounter' and 'context' fields as checked by the doesResourceReferToEncounter function in packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts. No broadening of encounter reference detection is needed since the current implementation matches Athena's exact data structure.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/core/src/external/fhir/shared/references.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-05-28T19:22:09.281Z
Learnt from: thomasyopes
PR: metriport/metriport#3882
File: packages/core/src/external/ehr/command/get-bundle-by-resource-type/ehr-get-bundle-by-resource-type-cloud.ts:27-49
Timestamp: 2025-05-28T19:22:09.281Z
Learning: In packages/core/src/external/ehr/command/get-bundle-by-resource-type/ehr-get-bundle-by-resource-type-cloud.ts, the EHR get bundle by resource type Lambda endpoint is guaranteed to return valid JSON, so JSON.parse() error handling is not necessary for this specific endpoint.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-06-06T16:45:31.832Z
Learnt from: thomasyopes
PR: metriport/metriport#3970
File: packages/api/src/external/ehr/athenahealth/command/write-back/medication.ts:17-17
Timestamp: 2025-06-06T16:45:31.832Z
Learning: The writeMedicationToChart function in packages/api/src/external/ehr/athenahealth/command/write-back/medication.ts returns a response that is not currently used by any consumers, so changes to its return type are not breaking changes in practice.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/api/src/command/mapping/patient.ts
  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-05-28T19:20:47.442Z
Learnt from: thomasyopes
PR: metriport/metriport#3882
File: packages/core/src/external/ehr/lambdas/get-bundle-by-resource-type/ehr-get-bundle-by-resource-type-cloud.ts:0-0
Timestamp: 2025-05-28T19:20:47.442Z
Learning: In packages/core/src/external/ehr/lambdas/get-bundle-by-resource-type/ehr-get-bundle-by-resource-type-cloud.ts, the EHR get bundle by resource type Lambda endpoint is guaranteed to return valid JSON, so JSON.parse() error handling is not necessary for this specific endpoint.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-08-11T23:36:57.573Z
Learnt from: thomasyopes
PR: metriport/metriport#4315
File: packages/core/src/external/ehr/healthie/index.ts:622-657
Timestamp: 2025-08-11T23:36:57.573Z
Learning: In the Healthie EHR integration's `getResourceBundleByResourceId` method, returning an empty bundle when a resource doesn't exist (by using `fetchResourcesFromEhr: () => Promise.resolve([])`) is intentional design. Empty bundles are an acceptable state for non-existent resources rather than throwing NotFoundError.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
📚 Learning: 2025-05-20T21:26:26.804Z
Learnt from: leite08
PR: metriport/metriport#3814
File: packages/api/src/routes/internal/medical/patient-consolidated.ts:141-174
Timestamp: 2025-05-20T21:26:26.804Z
Learning: The functionality introduced in packages/api/src/routes/internal/medical/patient-consolidated.ts is planned to be refactored in downstream PR #3857, including improvements to error handling and validation.

Applied to files:

  • packages/api/src/routes/medical/document.ts
  • packages/api/src/command/mapping/patient.ts
  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts
📚 Learning: 2025-06-18T21:05:22.256Z
Learnt from: RamilGaripov
PR: metriport/metriport#3976
File: packages/api/src/routes/medical/patient.ts:541-543
Timestamp: 2025-06-18T21:05:22.256Z
Learning: In packages/api/src/routes/medical/patient.ts, inline schema definitions like cohortIdSchema are acceptable and don't need to be moved to separate schema files when the user prefers to keep them inline.

Applied to files:

  • packages/api/src/routes/medical/document.ts
📚 Learning: 2025-06-13T20:24:16.668Z
Learnt from: thomasyopes
PR: metriport/metriport#3997
File: packages/api/src/routes/internal/medical/patient-job.ts:28-29
Timestamp: 2025-06-13T20:24:16.668Z
Learning: In packages/api/src/routes/internal/medical/patient-job.ts, mounting patientJobsRouter at the root path before other route handlers is intentional behavior, even though it could potentially consume requests before later handlers.

Applied to files:

  • packages/api/src/routes/medical/document.ts
📚 Learning: 2025-06-18T18:50:40.968Z
Learnt from: keshavsaharia
PR: metriport/metriport#4045
File: packages/core/src/external/surescripts/fhir/shared.ts:87-93
Timestamp: 2025-06-18T18:50:40.968Z
Learning: The `getResourceFromResourceMap` function in `packages/core/src/external/surescripts/fhir/shared.ts` works correctly as implemented. The comparison `resourceValue === resourceMap[key]` where `resourceValue = resource[key]` is intentional and functions as designed, despite appearing to compare different types.

Applied to files:

  • packages/core/src/external/fhir/shared/index.ts
📚 Learning: 2025-06-19T22:44:49.393Z
Learnt from: thomasyopes
PR: metriport/metriport#4061
File: packages/api/src/external/ehr/shared/job/bundle/create-resource-diff-bundles/run-job.ts:34-47
Timestamp: 2025-06-19T22:44:49.393Z
Learning: In packages/api/src/external/ehr/shared/job/bundle/create-resource-diff-bundles/run-job.ts, the team prefers to keep refreshEhrBundles operations grouped using fire-and-forget pattern rather than awaiting all operations with Promise.allSettled(). This allows individual refresh operations to run independently without blocking the job runner.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-04-21T03:47:54.332Z
Learnt from: lucasdellabella
PR: metriport/metriport#3608
File: packages/core/src/external/ehr/resource-diff/steps/start/ehr-start-resource-diff-local.ts:0-0
Timestamp: 2025-04-21T03:47:54.332Z
Learning: In the EHR resource diff implementation, bundles should always have an `entry` property, and code should be allowed to throw if `entry` is missing rather than using null-coalescing operators, as this represents an unexpected state that should be caught and addressed.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-05-28T02:32:27.527Z
Learnt from: lucasdellabella
PR: metriport/metriport#3907
File: packages/core/src/external/fhir/adt-encounters.ts:118-143
Timestamp: 2025-05-28T02:32:27.527Z
Learning: In packages/core/src/external/fhir/adt-encounters.ts, conversion bundles stored by saveAdtConversionBundle do not require version ID assertions or versioning, unlike the sourced encounter data stored by putAdtSourcedEncounter which does require versionId validation.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-05-08T19:41:36.533Z
Learnt from: thomasyopes
PR: metriport/metriport#3788
File: packages/api/src/external/ehr/shared/utils/bundle.ts:83-93
Timestamp: 2025-05-08T19:41:36.533Z
Learning: In the Metriport codebase, the team prefers to let errors bubble up naturally in some cases rather than adding explicit error handling at every layer, as demonstrated in the refreshEhrBundle function in the bundle.ts file.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
📚 Learning: 2025-05-19T18:24:41.632Z
Learnt from: thomasyopes
PR: metriport/metriport#3870
File: packages/core/src/external/ehr/bundle/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-local.ts:55-91
Timestamp: 2025-05-19T18:24:41.632Z
Learning: In EHR bundles related to resource diff computations, bundle creation operations that are for auditing purposes should not stop the main job flow if they fail. These operations typically use try/catch blocks that log errors without rethrowing them, which is the intended behavior.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/bundle/bundle-shared.ts
📚 Learning: 2025-04-23T21:19:02.589Z
Learnt from: thomasyopes
PR: metriport/metriport#3709
File: packages/api/src/external/ehr/canvas/command/bundle/fetch-ehr-bundle.ts:63-72
Timestamp: 2025-04-23T21:19:02.589Z
Learning: In the metriport codebase, factory functions like `fetchCanvasBundle` in the Canvas EHR subsystem intentionally let errors bubble up to the caller rather than handling them at the factory function level.

Applied to files:

  • packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
📚 Learning: 2025-08-13T21:36:10.069Z
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts:36-41
Timestamp: 2025-08-13T21:36:10.069Z
Learning: For contributionEncounterSummariesEnabled in Athena secondary mappings, strict boolean checking (=== true) is unnecessary because Zod schema validation ensures the value is boolean or undefined, making truthy checks sufficient.

Applied to files:

  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
📚 Learning: 2025-08-13T21:36:10.069Z
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts:36-41
Timestamp: 2025-08-13T21:36:10.069Z
Learning: In Athena EHR integration, when contributionEncounterAppointmentTypesBlacklist is present (even if empty), attachAppointmentType should be enabled to start tracking appointment type IDs for future use, rather than only enabling when the array has items.

Applied to files:

  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
📚 Learning: 2025-06-11T21:39:26.805Z
Learnt from: thomasyopes
PR: metriport/metriport#4000
File: packages/core/src/external/ehr/athenahealth/index.ts:504-507
Timestamp: 2025-06-11T21:39:26.805Z
Learning: In AthenaHealth write-back (`packages/core/src/external/ehr/athenahealth/index.ts`), only the condition statuses “relapse” and “recurrence” are currently mapped to AthenaHealth problem statuses (“CHRONIC”); other FHIR clinicalStatus values (e.g., “active”, “resolved”, “inactive”, “remission”) are not yet supported.

Applied to files:

  • packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts
📚 Learning: 2025-06-20T15:35:00.546Z
Learnt from: thomasyopes
PR: metriport/metriport#3891
File: packages/api/src/routes/internal/ehr/patient.ts:142-142
Timestamp: 2025-06-20T15:35:00.546Z
Learning: In the EHR resource diff contribution system, there are no "invalid" resource types. The system is designed to gracefully handle any resource type string by attempting to find a corresponding bundle, and if no bundle exists for that resource type, it will simply exit early rather than throwing an error.

Applied to files:

  • packages/core/src/external/ehr/bundle/bundle-shared.ts
📚 Learning: 2025-05-08T19:45:07.783Z
Learnt from: thomasyopes
PR: metriport/metriport#3788
File: packages/api/src/external/ehr/shared/command/bundle/fetch-bundle-presignd-urls.ts:45-62
Timestamp: 2025-05-08T19:45:07.783Z
Learning: In the Metriport codebase, the preferred error handling strategy is to let errors bubble up rather than catching and handling them at lower levels, especially in bundle operations like `fetchBundlePreSignedUrls`.

Applied to files:

  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
📚 Learning: 2025-04-23T18:57:54.964Z
Learnt from: thomasyopes
PR: metriport/metriport#3608
File: packages/api/src/external/ehr/canvas/command/bundle/fetch-ehr-bundle.ts:62-72
Timestamp: 2025-04-23T18:57:54.964Z
Learning: In the metriport codebase, factory implementations like `fetchCanvasBundle` intentionally let errors bubble up to the caller rather than handling them at the factory function level.

Applied to files:

  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
📚 Learning: 2025-04-23T21:19:08.443Z
Learnt from: thomasyopes
PR: metriport/metriport#3709
File: packages/api/src/external/ehr/canvas/command/bundle/fetch-resource-diff-bundle.ts:62-73
Timestamp: 2025-04-23T21:19:08.443Z
Learning: In the metriport codebase, factory functions like `fetchResourceDiffBundle` intentionally let errors bubble up to the caller rather than handling them at the factory function level. This is a consistent pattern across factory implementations.

Applied to files:

  • packages/core/src/external/ehr/bundle/command/fetch-bundle.ts
🧬 Code Graph Analysis (9)
packages/core/src/external/ehr/document/command/fetch-document.ts (2)
packages/core/src/external/ehr/document/document-shared.ts (2)
  • DocumentKeyBaseParams (47-57)
  • createKeyAndExtensionMap (35-41)
packages/core/src/util/log.ts (1)
  • out (30-35)
packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts (2)
packages/core/src/external/ehr/api/get-secondary-mappings.ts (1)
  • getSecondaryMappings (17-41)
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (1)
  • athenaSecondaryMappingsSchema (4-13)
packages/api/src/routes/medical/document.ts (1)
packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (1)
  • getUploadUrlAndCreateDocRef (15-56)
packages/core/src/external/ehr/document/command/create-or-replace-document.ts (1)
packages/shared/src/index.ts (1)
  • BadRequestError (42-42)
packages/core/src/external/fhir/shared/references.ts (1)
packages/core/src/external/fhir/shared/index.ts (1)
  • buildResourceReference (307-309)
packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (12)
packages/api/src/command/mapping/cx.ts (1)
  • getCxMappingOrFail (54-63)
packages/shared/src/index.ts (2)
  • MetriportError (43-43)
  • errorToString (45-45)
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (1)
  • athenaSecondaryMappingsSchema (4-13)
packages/core/src/external/ehr/athenahealth/index.ts (1)
  • getEncounterAppointmentTypeIdExtension (175-180)
packages/core/src/external/fhir/shared/index.ts (1)
  • buildResourceReference (307-309)
packages/core/src/external/ehr/bundle/command/create-or-replace-bundle.ts (1)
  • createOrReplaceBundle (32-96)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • createBundleFromResourceList (56-61)
packages/core/src/util/log.ts (1)
  • out (30-35)
packages/core/src/external/ehr/document/command/fetch-document.ts (1)
  • fetchDocument (40-97)
packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (1)
  • getUploadUrlAndCreateDocRef (15-56)
packages/core/src/external/ehr/document/command/create-or-replace-document.ts (1)
  • createOrReplaceDocument (33-89)
packages/lambdas/src/shared/capture.ts (1)
  • capture (19-118)
packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts (2)
packages/core/src/external/ehr/api/get-secondary-mappings.ts (1)
  • getSecondaryMappings (17-41)
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (1)
  • athenaSecondaryMappingsSchema (4-13)
packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (4)
packages/core/src/external/aws/s3.ts (1)
  • S3Utils (140-570)
packages/shared/src/util/uuid-v7.ts (1)
  • uuidv7 (369-371)
packages/core/src/util/log.ts (1)
  • out (30-35)
packages/core/src/domain/document/filename.ts (1)
  • createDocumentFilePath (11-18)
packages/core/src/external/ehr/athenahealth/index.ts (2)
packages/core/src/external/ehr/document/command/fetch-document.ts (1)
  • fetchDocument (40-97)
packages/core/src/external/ehr/document/command/create-or-replace-document.ts (1)
  • createOrReplaceDocument (33-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e-tests / E2E Tests
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (32)
packages/api/Dockerfile.dev (1)

13-13: Good addition: ensure fhir-sdk is built into the image

Adding the fhir-sdk build step aligns with the repo’s pattern where SDK /dist artifacts are produced at build time (per prior learnings) and prevents runtime “module not found” issues when code starts importing it.

packages/core/src/external/ehr/document/command/create-or-replace-document.ts (1)

53-55: LGTM: defensive guard now clearly scoped.

The added braces around the throw improve readability without changing behavior.

docs/medical-api/api-reference/document/post-upload-url.mdx (1)

140-147: Example update to PDF upload looks good.

The guidance to use Content-Type: application/pdf and the renamed fileContent variable align with the new upload flow.

packages/api/src/command/mapping/patient.ts (1)

97-102: Verify Healthie support in parseExternalId & downstream flows

File: packages/api/src/command/mapping/patient.ts
Lines: 97–102

export const defaultSources = [
  EhrSources.athena,
  EhrSources.canvas,
  EhrSources.elation,
  EhrSources.healthie,
];

Please confirm the following:

  • parseExternalId (in external-id.ts) only special-cases EhrSources.athena and otherwise returns the raw ID. Verify that falling through for EhrSources.healthie is correct.
  • Any calls to getSourceMapForPatient(…) that omit the sources parameter now include healthie. Locate all single-argument calls and ensure those consumers handle Healthie inputs without unintended data fan-out.
packages/core/src/external/fhir/shared/index.ts (1)

301-302: Rename verified: no remaining buildEntryReference usages

All occurrences of buildEntryReference have been replaced with buildResourceReference across the repository.

  • Confirmed no calls to buildEntryReference(
  • Confirmed no imports of buildEntryReference

No further action required.

packages/core/src/external/fhir/shared/references.ts (2)

12-12: Import rename aligns with shared helper; safe change

Switching to buildResourceReference centralizes reference formatting and keeps behavior consistent with toReference in this module.


113-114: Reference construction is correct for FHIR Encounter-related components

Using buildResourceReference(resource) to populate the Reference.reference string (e.g., "Practitioner/123") is appropriate for EncounterParticipant.individual, EncounterLocation.location, and EncounterDiagnosis.condition.

Also applies to: 122-123, 131-132

packages/core/src/external/ehr/bundle/bundle-shared.ts (3)

47-51: New file key for “contribution removed” bundles is consistent

Naming and path convention match existing keys and prefixing.


81-81: Enum extension for removal bundle type looks good

RESOURCE_DIFF_DATA_CONTRIBUTION_REMOVED integrates cleanly with existing BundleType.


109-111: Key map entry for removal bundle is correct

Mapping to createFileKeyResourceDiffDataContributionRemoved is consistent with others.

packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (4)

70-81: Hard-fail on missing Athena secondary mappings matches intended behavior

The fetch of CX mapping by practiceId, schema parse, and explicit MetriportError when missing are aligned with prior decisions to avoid silent defaults.


176-184: Partition-based removal is correct and efficient

Using a Set for membership and lodash.partition to split keep/remove lists is clean and O(n).


233-272: Auditing bundle creation failure is non-fatal by design

Catching and logging errors while creating the “contribution removed” bundle matches the established pattern for audit artifacts not blocking main flow.


205-220: Encounter reference detection misses DocumentReference.context.encounter[]; fix shape handling

doesResourceReferenceEncounter currently checks:

  • resource.encounter.reference (ok for single Reference)
  • resource.context.reference (incorrect: DocumentReference.context is an object whose encounter is Reference[])

This will fail to remove DocumentReferences linked to excluded Encounters, leaving dangling artifacts.

Refactor to handle both single Reference and arrays under context.encounter.

Apply this diff:

 function doesResourceReferenceEncounter(
   resource: Resource,
   encounterReferences: string[]
 ): boolean {
-  if ("encounter" in resource) {
-    const encounter = resource.encounter;
-    if (!encounter || !encounter.reference) return false;
-    return encounterReferences.includes(encounter.reference);
-  }
-  if ("context" in resource) {
-    const context = resource.context;
-    if (!context || !("reference" in context) || !context.reference) return false;
-    return encounterReferences.includes(context.reference);
-  }
-  return false;
+  const asAny = resource as any;
+  // Handle Resource.encounter as Reference or Reference[]
+  if (asAny.encounter) {
+    const refs = Array.isArray(asAny.encounter) ? asAny.encounter : [asAny.encounter];
+    if (refs.some((r: any) => r?.reference && encounterReferences.includes(r.reference))) {
+      return true;
+    }
+  }
+  // Handle DocumentReference.context.encounter as Reference[]
+  const ctx = asAny.context;
+  if (ctx?.encounter) {
+    const ctxRefs = Array.isArray(ctx.encounter) ? ctx.encounter : [ctx.encounter];
+    if (ctxRefs.some((r: any) => r?.reference && encounterReferences.includes(r.reference))) {
+      return true;
+    }
+  }
+  return false;
 }
⛔ Skipped due to learnings
Learnt from: thomasyopes
PR: metriport/metriport#4346
File: packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts:221-233
Timestamp: 2025-08-14T20:43:26.281Z
Learning: In Athena EHR integration, encounter references are housed specifically in the 'encounter' and 'context' fields as checked by the doesResourceReferToEncounter function in packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts. No broadening of encounter reference detection is needed since the current implementation matches Athena's exact data structure.
packages/core/src/external/ehr/document/command/fetch-document.ts (3)

23-24: Constant for URL validity is appropriate

documentUrlDuration = 1 hour is centralized and reused; good for consistency.


40-97: Robust S3 fetch with retries and contextual errors

  • Validates documentType via map
  • Early-exits on missing file
  • Batches content and metadata fetch under executeWithNetworkRetries
  • Throws MetriportError with rich context

LGTM.


116-169: S3Utils.getSignedUrl parameter names align with implementation

The parameter names bucketName, fileName, and durationSeconds in your call match the getSignedUrl signature exactly. No changes needed.

packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts (2)

26-33: Hard-fail on secondary mappings fetch for Encounters — LGTM

Fetching Athena secondary mappings only for Encounters and allowing failures to bubble up aligns with prior decision to hard-fail when mappings control critical behavior. Good call.


41-46: Athena client already supports both new flags

I’ve confirmed that in packages/core/src/external/ehr/athenahealth/index.ts the AthenaHealthApi methods getResourceBundleByResourceId and getBundleByResourceType declare, default, and wire through both attachAppointmentType and fetchEncounterSummary. No further changes needed here.

packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts (2)

25-33: Hard-fail on mappings fetch for Encounters — LGTM

Same pattern here as per-resource-id: fetching mappings only for Encounters and letting failures bubble up is consistent with the chosen approach. Looks good.


39-44: Parity Confirmed for Athena Command Flags

Shell diff shows both

  • packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts
  • packages/core/src/external/ehr/athenahealth/command/get-resource-bundle-by-resource-id.ts

include identical mapping logic:

  • mappings?.contributionEncounterAppointmentTypesBlacklistattachAppointmentType: true
  • mappings?.contributionEncounterSummariesEnabledfetchEncounterSummary: true

No discrepancies found—no further action needed.

packages/core/src/external/ehr/bundle/command/fetch-bundle.ts (2)

52-53: S3 bucket paramization + richer error context — LGTM

Adding s3BucketName with a sensible default, threading it through S3 calls, and enriching error context (including the bucket and key) improves operability and debuggability.

Also applies to: 75-83, 88-102


164-176: Pre-signed URL path improvements — LGTM

Clearer error message and inclusion of key and s3BucketName in the context are solid improvements.

packages/api/src/routes/medical/document.ts (3)

232-241: Centralizing upload orchestration — LGTM

Validating via docRefCheck here and delegating to the shared command is the right move. This removes route-level S3/FHIR coupling and improves reuse.


279-287: New /upload route return payload — LGTM

Returning { documentReferenceId, uploadUrl } from the centralized command is a clean contract and will help clients track the DocumentReference immediately.


256-265: Verify deprecated /upload-url response shape and update docs/tests
I didn’t find any references in docs (MD/MDX), code, or tests for POST /document/upload-url. Please:

  • Confirm existing clients still expect the raw URL string response
  • Update any documentation or automated tests to reflect this behavior if they rely on /upload-url
packages/api/src/command/medical/document/get-upload-url-and-create-doc-ref.ts (1)

15-25: Command signature and overall flow — LGTM

Clear contract, deterministic behavior, parallelizing FHIR upsert with URL generation, and no unnecessary arrow functions. Good adherence to the codebase patterns.

packages/core/src/external/ehr/athenahealth/index.ts (5)

6-8: Imports look good and purposeful.

Aliasing Encounter to EncounterFhir and adding Extension is appropriate for the new helper, and the new fetchDocument import is used correctly below.

Also applies to: 123-123


172-181: Helper for extracting appointment-type extension: solid addition.

Encapsulating the URL and providing a typed accessor increases reuse and readability. No issues spotted.


1609-1620: New flags on getResourceBundleByResourceId: API remains backward compatible.

Defaults and typing look good. No concerns here.


1976-1976: Encounter narrowing check: LGTM.

The guard correctly ensures only Encounter resources are processed.


1504-1514: Mapping logic validated—flags correctly driven by secondary mappings

Verified that in packages/core/src/external/ehr/athenahealth/command/get-bundle-by-resource-type.ts (and the analogous get-resource-bundle-by-resource-id.ts):

  • attachAppointmentType is enabled whenever mappings.contributionEncounterAppointmentTypesBlacklist is present (empty array is truthy in JS/TS), matching the learning.
  • fetchEncounterSummary is enabled only when mappings.contributionEncounterSummariesEnabled is true.

No changes required.

Comment on lines +81 to +93
if (secondaryMappings.contributionEncounterAppointmentTypesBlacklist) {
await dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType({
ehr,
cxId,
metriportPatientId,
ehrPatientId,
resourceType,
jobId,
bundle: bundle.bundle,
blacklistedAppointmentTypes:
secondaryMappings.contributionEncounterAppointmentTypesBlacklist,
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Presence vs. emptiness of blacklist: verify intent to remove encounters without extension

Currently, if contributionEncounterAppointmentTypesBlacklist is present (even empty), the code proceeds to removal with dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType. That function removes:

  • Encounters whose appointmentType is in the blacklist
  • Encounters that do NOT have the appointment type extension at all

With an empty blacklist, the latter behavior will remove all encounters missing the extension (and cascade-remove their referencing resources). Given rollout timing, this could aggressively prune bundles where the extension hasn’t propagated yet.

If the intended behavior is “presence enables tracking but does not remove anything when the array is empty,” adjust the filter to only remove when an extension is present and matches the blacklist.

Apply this diff inside dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType to avoid removing encounters solely due to missing extension:

-    .filter((encounter: Encounter) => {
-      const appointmentTypeExtension = getEncounterAppointmentTypeIdExtension(encounter);
-      if (!appointmentTypeExtension || !appointmentTypeExtension.valueString) return true;
-      return blacklistedAppointmentTypes.includes(appointmentTypeExtension.valueString);
-    })
+    .filter((encounter: Encounter) => {
+      const appointmentTypeExtension = getEncounterAppointmentTypeIdExtension(encounter);
+      // Only remove when the extension exists and matches the blacklist
+      if (!appointmentTypeExtension || !appointmentTypeExtension.valueString) return false;
+      return blacklistedAppointmentTypes.includes(appointmentTypeExtension.valueString);
+    })

If you do want the “remove encounters missing the extension” behavior, consider gating it behind a separate mapping flag (e.g., dropEncountersWithoutAppointmentType: boolean).


🏁 Script executed:

#!/bin/bash
# Locate the implementation of dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType
rg -n "dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType" -C5

Length of output: 2839


🏁 Script executed:

#!/bin/bash
# Show filter logic in dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType
echo "=== Filter logic ==="
rg -nP '\.filter\(' -C3 packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts

# Locate the definition of the secondary mappings schema to see defaults/optionality
echo ""
echo "=== Athena Secondary Mappings Schema ==="
rg -n "athenaSecondaryMappingsSchema" -C5 packages/api/src

Length of output: 7320


🏁 Script executed:

#!/bin/bash
# Locate the Athena secondary mappings schema definition
rg -n "athenaSecondaryMappingsSchema" -C5 .

# Search for any attachAppointmentType flag in the mapping schema or usages
rg -n "attachAppointmentType" -C5 .

Length of output: 25597


Prevent blanket removal when the blacklist is empty

The current logic will remove every encounter lacking the appointment-type extension as soon as contributionEncounterAppointmentTypesBlacklist is set—even if it’s an empty array. This can inadvertently prune all encounters when the mapping is present but no IDs are blacklisted.

• In packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts (around line 81), only call the removal step when there are actual blacklist entries:

Change
if (secondaryMappings.contributionEncounterAppointmentTypesBlacklist) {
To
if (secondaryMappings.contributionEncounterAppointmentTypesBlacklist?.length > 0) {

• Or, inside dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType (around lines 159–162), adjust the filter so it only removes encounters whose extension exists and matches the blacklist:

   .filter((encounter: Encounter) => {
-      const appointmentTypeExtension = getEncounterAppointmentTypeIdExtension(encounter);
-      if (!appointmentTypeExtension || !appointmentTypeExtension.valueString) return true;
-      return blacklistedAppointmentTypes.includes(appointmentTypeExtension.valueString);
+      const appointmentTypeExtension = getEncounterAppointmentTypeIdExtension(encounter);
+      // only drop when the extension is present and in the blacklist
+      if (!appointmentTypeExtension?.valueString) return false;
+      return blacklistedAppointmentTypes.includes(appointmentTypeExtension.valueString);
  })

• If you still need to prune encounters missing the extension, introduce a separate flag (e.g. dropEncountersWithoutAppointmentType) instead of tying that behavior to the blacklist’s presence.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
packages/api/src/external/ehr/shared/command/bundle/contribute-resource-diff-bundle.ts
around lines 81–93, the current if-check triggers blanket removal whenever
contributionEncounterAppointmentTypesBlacklist is present (even when it's an
empty array); update the call site to only run
dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType when the
blacklist actually contains entries (e.g. check .length > 0), or alternatively
modify dangerouslyRemoveEncounterEntriesWithBlacklistedAppointmentType (around
its implementation lines ~159–162) so the filter only removes encounters whose
appointment-type extension exists and matches a blacklisted id (and do not
remove encounters that lack the extension unless an explicit flag like
dropEncountersWithoutAppointmentType is provided); implement one of these fixes
and ensure tests cover empty-array behavior.

Comment on lines +1981 to +2001
let encounter: Encounter | undefined;
if (attachAppointmentType) {
encounter = await this.getEncounter({
cxId,
patientId: athenaPatientId,
encounterId,
});
await createOrReplaceDocument({
ehr: EhrSources.athena,
const appointment = await this.getAppointment({
cxId,
metriportPatientId,
ehrPatientId: athenaPatientId,
documentType: DocumentType.HTML,
payload: encounterSummary,
resourceType: "Encounter",
resourceId: encounterId,
patientId: athenaPatientId,
appointmentId: encounter.appointmentid,
});
entry.resource.extension = [
...(entry.resource.extension ?? []),
{
url: encounterAppointmentExtensionUrl,
valueString: appointment.appointmenttypeid,
},
];
}
if (fetchEncounterSummary) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make appointment-type extension idempotent and guard missing IDs.

Currently we always append a new extension, which can duplicate entries across repeated fetches or cache hits. Also, we don’t guard against missing appointmentid/appointmenttypeid. Use the helper to update-in-place if present, or append only once.

Apply this diff:

-          let encounter: Encounter | undefined;
+          let encounter: Encounter | undefined;
           if (attachAppointmentType) {
             encounter = await this.getEncounter({
               cxId,
               patientId: athenaPatientId,
               encounterId,
             });
-            const appointment = await this.getAppointment({
+            if (!encounter.appointmentid) return;
+            const appointment = await this.getAppointment({
               cxId,
-              patientId: athenaPatientId,
-              appointmentId: encounter.appointmentid,
+              patientId: athenaPatientId,
+              appointmentId: encounter.appointmentid,
             });
-            entry.resource.extension = [
-              ...(entry.resource.extension ?? []),
-              {
-                url: encounterAppointmentExtensionUrl,
-                valueString: appointment.appointmenttypeid,
-              },
-            ];
+            const existingExt = getEncounterAppointmentTypeIdExtension(
+              entry.resource as EncounterFhir
+            );
+            const typeId = appointment.appointmenttypeid;
+            if (!typeId) return;
+            if (existingExt) {
+              existingExt.valueString = String(typeId);
+            } else {
+              entry.resource.extension = [
+                ...(entry.resource.extension ?? []),
+                {
+                  url: encounterAppointmentExtensionUrl,
+                  valueString: String(typeId),
+                },
+              ];
+            }
           }
🤖 Prompt for AI Agents
In packages/core/src/external/ehr/athenahealth/index.ts around lines 1981 to
2001, the code always appends an appointment-type extension which can create
duplicates and does not guard for missing appointmentid or appointmenttypeid;
update the logic to first verify encounter and appointment exist and that
encounter.appointmentid and appointment.appointmenttypeid are present, then use
the existing helper to upsert (update-in-place) the extension if one with url
encounterAppointmentExtensionUrl already exists, otherwise append it once;
ensure you do not append duplicates on repeated calls and handle missing IDs by
skipping the extension addition when IDs are absent.

Orta21
Orta21 previously approved these changes Aug 19, 2025
Thomas Yopes and others added 2 commits August 19, 2025 16:56
…backmerge

Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
@thomasyopes thomasyopes added this pull request to the merge queue Aug 20, 2025
Merged via the queue into master with commit 1b214f0 Aug 20, 2025
67 of 68 checks passed
@thomasyopes thomasyopes changed the title RELEASE athena contribution w/ appoint type id filtering and encounter summaries RELEASE athena contribution w/ appointment type id filtering and encounter summaries Aug 20, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 5, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants