Skip to content

Conversation

alexeysergeev-cm
Copy link
Contributor

@alexeysergeev-cm alexeysergeev-cm commented Aug 15, 2025

Issues:

Dependencies

  • Upstream: none
  • Downstream: none

Description

  • Wire athena auto write-back flow

Testing

  • Local
    • call the flow locally to make sure no errors
    • vitals - no new filtering so just general check
    • labs - absolute date
    • conditions - string filtering works
    • medication statements - absolute date + rxnorm filtering
  • Staging
    • run all four write-back resources on a staging chris smith
  • Sandbox
    • N/A
  • Production
    • cx test

Release Plan

  • Contains a DB migration! ⚠️
  • Update all patient mapping rows with an empty object once shipped update patient_mapping set secondary_mappings = '{}'::jsonb;
  • Merge this

Summary by CodeRabbit

  • New Features

    • Added Athena write-back for conditions, labs, grouped vitals, and medication statements.
    • Introduced medication-statement as a supported write-back resource.
    • Implemented EHR-specific grouped-vitals routing (code-based for Athena, date-based for Elation).
    • Added endpoints and helpers to read/update patient secondary mappings; auto-resolve/validate Athena department; Elation default practice/physician helper.
    • Expanded write-back filters (labs, lab panels, conditions, medication statements).
  • Chores

    • Added secondary_mappings column and propagated through patient-mapping flows.
  • Refactor

    • Switched Athena patient/custom-fields retrieval to Athena One; updated internal wiring.
  • Tests

    • Updated to pass EHR context in write-back decisions.

Copy link

linear bot commented Aug 15, 2025

Copy link

coderabbitai bot commented Aug 15, 2025

Warning

Rate limit exceeded

@thomasyopes has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 24 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 84b0481 and c7b5a00.

📒 Files selected for processing (5)
  • packages/api/src/external/ehr/athenahealth/command/update-patient-mapping-department-id.ts (1 hunks)
  • packages/api/src/sequelize/migrations/2025-09-05_00_alter-patient-mapping-add-secondary-mappings.ts (1 hunks)
  • packages/core/src/external/ehr/command/write-back/shared.ts (1 hunks)
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (21 hunks)
  • packages/core/src/external/ehr/shared.ts (4 hunks)

Walkthrough

Adds Athena write-back handlers (condition, lab, grouped-vitals, medication-statement) and dispatch wiring. Introduces secondary mappings for patient mappings (schemas, API, DB column, routes, sync call-sites). Implements Athena patient department-id retrieval/validation and update flow. Refactors grouped-vitals (union typing, grouping utilities). Extends write-back bundle logic with medication paths and new skip filters. Updates Athena API to AthenaOne patients.

Changes

Cohort / File(s) Summary
Athena write-back handlers
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts, .../write-back/lab.ts, .../write-back/grouped-vitals.ts, .../write-back/medication-statement.ts
New functions to write back condition, lab, grouped vitals, and medication statements. Each resolves departmentId via getAndCheckAthenaPatientDepartmentId, creates client, and calls respective create endpoints.
Write-back dispatch wiring
packages/core/src/external/ehr/command/write-back/condition.ts, .../write-back/lab.ts, .../write-back/grouped-vitals.ts, .../write-back/medication-statement.ts
Maps Athena to new handlers. For grouped-vitals: introduces EhrGroupedVitals union, grouping utilities, and getEhrGroupedVitals; removes older EHR guard exports.
Grouped vitals shared types/utilities
packages/core/src/external/ehr/shared.ts, packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts, packages/core/src/external/ehr/elation/index.ts
Adds GroupedVitalsByDate, GroupedObservation, title and BP handling helpers. Elation grouped-vitals now validates date-based grouping and fetches default IDs via new helper. Import path for GroupedVitalsByDate adjusted.
Elation default IDs helper
packages/core/src/external/ehr/elation/command/get-default-practice-and-physician-ids.ts, .../write-back/lab.ts, .../write-back/lab-panel.ts
New helper to fetch default practice/physician IDs from secondary mappings; lab and lab-panel write-backs updated to use it.
Patient secondary mappings feature (API/domain/model/migration)
packages/shared/src/interface/external/ehr/shared.ts, packages/shared/src/interface/external/ehr/athenahealth/patient-mapping.ts, packages/core/src/external/ehr/mappings.ts, packages/api/src/domain/patient-mapping.ts, packages/api/src/models/patient-mapping.ts, packages/api/src/sequelize/migrations/2025-08-31_00_alter-patient-mapping-add-secondary-mappings.ts
Adds schemas/types for secondary mappings (general and Athena-specific), schema map per EHR, model column secondary_mappings (JSONB) with raw transform, and migration to add column.
Patient secondary mappings API and helpers
packages/api/src/command/mapping/patient.ts, packages/api/src/routes/internal/ehr/patient.ts, packages/core/src/external/ehr/api/get-patient-secondary-mappings.ts, packages/api/src/external/ehr/shared/command/mapping/get-patient-mapping-and-secondary-mappings.ts
Extend patient mapping commands to accept/store secondaryMappings; new endpoint to fetch secondary mappings; core API client to retrieve and validate them; helper to fetch mapping and parse secondary mappings.
Sync call-sites now pass secondaryMappings
packages/api/src/external/ehr/*/command/sync-patient.ts (athenahealth, canvas, eclinicalworks, elation, healthie), packages/api/src/command/medical/patient/get-quest-roster.ts, packages/api/src/routes/internal/medical/patient.ts
findOrCreate/createPatientMapping calls now include secondaryMappings (practiceId or {}), without other flow changes.
Athena departmentId retrieval/validation and update
packages/core/src/external/ehr/athenahealth/command/get-patient-department-id.ts, .../command/get-and-check-patient-department-id.ts, packages/core/src/external/ehr/athenahealth/api/update-secondary-mapping-department-id.ts, packages/api/src/external/ehr/athenahealth/command/update-patient-mapping-department-id.ts, packages/api/src/routes/internal/ehr/athenahealth/patient.ts
Adds logic to read departmentId from secondary mappings or fetch from Athena and persist via internal API; validates against enabled departments; new API endpoint and server-side command to update stored departmentId.
Athena API (AthenaOne patients, resources)
packages/core/src/external/ehr/athenahealth/index.ts, packages/shared/src/interface/external/ehr/athenahealth/patient.ts
Switches to AthenaOne patients schema; adds getAthenaOnePatient and getDepartmentIdForPatient; updates custom fields retrieval; includes MedicationStatement in supported resources and adjusts bundle fetch behavior.
Write-back resource diff bundles
packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts, .../__tests__/ehr-write-back-resource-diff-bundles-direct.test.ts
Adds medication statement support and new skip helpers (conditions string filters, absolute dates, RxNorm). Replaces grouped-vitals date grouping with getEhrGroupedVitals. Updates function signatures and test invocations to include ehr context.
Internal routing and minor cleanup
packages/api/src/routes/internal/index.ts, packages/api/src/command/mapping/cx.ts
Deduplicates questRoutes import; minor parameter ordering formatting in findOrCreateCxMapping.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant WB as writeBackMedicationStatement
  participant Dept as getAndCheckAthenaPatientDepartmentId
  participant GetDept as getPatientDepartmentId
  participant API as Core API (secondary mappings)
  participant AthCli as AthenaHealth Client
  participant Srv as API Server (update dept)

  Caller->>WB: params { cxId, practiceId, ehrPatientId, medication, statements, tokenInfo? }
  WB->>WB: createMedicationWithRefs()
  WB->>Dept: { cxId, practiceId, patientId, tokenInfo? }
  par Resolve dept
    Dept->>GetDept: { cxId, athenaPatientId, athenaPracticeId, tokenInfo? }
    alt dept in secondary mappings
      GetDept->>API: GET /internal/ehr/athenahealth/patient/{id}/secondary-mappings
      API-->>GetDept: { departmentId }
      GetDept-->>Dept: departmentId
    else not stored
      GetDept->>AthCli: getDepartmentIdForPatient()
      AthCli-->>GetDept: departmentId
      GetDept->>API: GET secondary mappings
      GetDept->>Srv: POST /internal/ehr/athenahealth/patient/secondary-mappings/department-id
      Srv-->>GetDept: 200
      GetDept-->>Dept: departmentId
    end
  and Validate dept
    Dept->>API: GET secondary mappings (enabled depts)
    API-->>Dept: { enabledDepartmentIds? }
    Dept-->>WB: departmentId (or throws if not enabled)
  end
  WB->>AthCli: createMedicationWithStatements({ cxId, ehrPatientId, departmentId, medicationWithRefs })
  AthCli-->>WB: 200
  WB-->>Caller: void
Loading
sequenceDiagram
  autonumber
  actor Job as Write-back Job
  participant Diff as shouldWriteBackResource
  participant Utils as Filters/Grouping
  participant Map as Dispatcher

  Job->>Diff: { ehr, resource, filters }
  alt Condition
    Diff->>Utils: skipConditionStringFilters()
  else Lab
    Diff->>Utils: skipLabDateAbsolute()
  else Lab Panel
    Diff->>Utils: skipLabPanelDateAbsolute()
  else MedicationStatement
    Diff->>Utils: skipMedicationRxnormCode()/skipMedicationStatementDateAbsolute()
  end
  Diff-->>Job: boolean

  Job->>Utils: getEhrGroupedVitals({ ehr, vitals })
  Utils-->>Job: EhrGroupedVitals[]
  Job->>Map: writeBackResource dispatch
  Map-->>Job: handler
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch eng-585-athena-auto-writeback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alexeysergeev-cm alexeysergeev-cm force-pushed the eng-585-athena-auto-writeback branch from 9b473be to 0703c6f Compare August 24, 2025 22:54
@alexeysergeev-cm alexeysergeev-cm changed the base branch from develop to eng-559-elation-writeback-followups August 24, 2025 22:55
@alexeysergeev-cm alexeysergeev-cm marked this pull request as ready for review August 25, 2025 00:06
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

🧹 Nitpick comments (11)
packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (1)

15-21: Minor: prefer safe destructuring for departmentId

Using array destructuring is a bit more idiomatic and guards against undefined arrays.

Apply this diff:

-  const athenaDepartmentId = secondaryMappings.departmentIds[0];
+  const [athenaDepartmentId] = secondaryMappings.departmentIds ?? [];
packages/core/src/external/ehr/shared.ts (2)

1009-1021: getValidCode may over-filter; consider allowing entries without display

Filtering out codings without a display can yield no titles even when valid codes exist. Suggest allowing codings with a code but missing display, and let the caller derive a fallback label.

Apply this diff:

-export function getValidCode(coding: Coding[] | undefined): Coding[] {
+export function getValidCode(coding: Coding[] | undefined): Coding[] {
   if (!coding) return [];
 
   return coding.filter(coding => {
     return (
       coding.code &&
       coding.code.toLowerCase().trim() !== UNK_CODE.toLowerCase() &&
-      coding.display &&
-      coding.display.toLowerCase().trim() !== UNKNOWN_DISPLAY
+      (coding.display ? coding.display.toLowerCase().trim() !== UNKNOWN_DISPLAY : true)
     );
   });
 }

1023-1041: Title normalization: broaden BP aliases and avoid mutating for non-S/D components

Current logic rewrites any title containing “blood pressure” to “Blood Pressure” and sets grouping = title. That’s fine for S/D components, but generic BP measurements (e.g., MAP) will later be dropped by the pairing routine. Keep grouping only for S/D, and let other BP-related observations preserve their title.

Apply this diff:

-  if (
-    title.toLowerCase().includes("blood pressure") ||
-    title.toLowerCase().includes("bp sys") ||
-    title.toLowerCase().includes("bp dias")
-  ) {
-    observationPoint.grouping = title;
-    updatedTitle = "Blood Pressure";
-  }
+  const lower = title.toLowerCase();
+  const isSystolic = lower.includes("systolic") || lower.includes("bp sys");
+  const isDiastolic = lower.includes("diastolic") || lower.includes("bp dias");
+  const isBloodPressure = lower.includes("blood pressure") || lower.startsWith("bp ");
+  if (isBloodPressure && (isSystolic || isDiastolic)) {
+    observationPoint.grouping = title;
+    updatedTitle = "Blood Pressure";
+  }
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (4)

70-72: Type guard: avoid 'in' per local guideline; use property presence check

Slight style tweak per guideline “Use truthy syntax instead of in”.

Apply this diff:

-export function isGroupedVitalsByCode(val: unknown): val is GroupedVitals {
-  return !!val && typeof val === "object" && "mostRecentObservation" in val;
-}
+export function isGroupedVitalsByCode(val: unknown): val is GroupedVitals {
+  const obj = val as Partial<GroupedVitals> | undefined;
+  return !!obj && typeof obj === "object" && obj.mostRecentObservation !== undefined;
+}

92-96: Ensure date-grouped output is sorted by date

Downstream consumers typically expect chronological order.

Apply this diff:

-  return Object.entries(groupedVitals).map(([chartDate, observations]) => [
-    buildDayjs(chartDate).toDate(),
-    observations,
-  ]);
+  return Object.entries(groupedVitals)
+    .sort((a, b) => buildDayjs(a[0]).valueOf() - buildDayjs(b[0]).valueOf())
+    .map(([chartDate, observations]) => [buildDayjs(chartDate).toDate(), observations]);

105-143: Use forEach instead of map for side effects

.map’s return value is unused; .forEach communicates intent and avoids creating an intermediate array.

Apply this diff:

-  observations.map(v => {
+  observations.forEach(v => {
     let title: string;
     const codings = getValidCode(v.code?.coding);
     const displays = codings.map(coding => coding.display);
     if (displays.length) {
       title = Array.from(new Set(displays)).join(", ");
     } else if (v.code?.text) {
       title = v.code.text;
     } else {
       results.push({ title: "-", mostRecentObservation: v });
       return;
     }
@@
-    const groupedObservation = observationMap.get(title);
-    if (groupedObservation) {
-      groupedObservation.push(observationPoint);
-      observationMap.set(title, groupedObservation);
-    } else {
-      observationMap.set(title, [observationPoint]);
-    }
-  });
+    const groupedObservation = observationMap.get(title);
+    if (groupedObservation) groupedObservation.push(observationPoint);
+    else observationMap.set(title, [observationPoint]);
+  });

148-164: Nit: prefer forEach over map when pushing to an array

The result of .map is not used; use .forEach for clarity.

Apply this diff:

-  Array.from(observationMap.entries()).map(([title, values]) => {
+  Array.from(observationMap.entries()).forEach(([title, values]) => {
     const sortedPoints = values.sort(
       (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
     );
     const mostRecent = sortedPoints[sortedPoints.length - 1];
     if (!mostRecent) return;
     mainResults.push({
       title,
       mostRecentObservation: mostRecent.rawVital,
       sortedPoints: sortedPoints.map(p => ({
         value: p.observation,
         date: p.date,
         ...(p.unit && { unit: p.unit }),
         ...(p.bp && { bp: p.bp }),
       })),
     });
-  });
+  });
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (2)

10-16: Department selection: picking the first ID is brittle when multiple are configured

If multiple departmentIds are present, always choosing index 0 may write to an unintended department. Consider allowing an explicit preferred department (if/when our mappings support it) or at least logging which one was chosen to aid traceability.

Example minimal improvement (retain behavior, add clarity):

-  const athenaDepartmentId = secondaryMappings.departmentIds[0];
+  const [athenaDepartmentId] = secondaryMappings.departmentIds ?? [];

Optionally, centralize resolution into a helper (see grouped-vitals comment) so all Athena write-backs share the same logic.


27-33: Optional: Pre-validate SNOMED code before calling Athena createProblem

Although our ehrWriteBackConditionPrimaryCodeMap correctly maps Athena to SNOMED_CODE, the Athena‐specific writeBackCondition command does not verify that the provided FHIR Condition actually contains a SNOMED coding. If it’s missing, Athena will return a generic 4xx—adding an explicit check will surface a clearer, early error.

• Location:
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (around lines 27–33)

• Suggested tweak:

 import { BadRequestError } from "@metriport/shared";
+import { getConditionSnomedCode } from "../../../shared";
 
 export async function writeBackCondition(params: WriteBackConditionClientRequest): Promise<void> {
   const { cxId, practiceId, ehrPatientId, tokenId, condition } = params;
+
+  // Optional: ensure the Condition has a SNOMED coding
+  const snomedCode = getConditionSnomedCode(condition);
+  if (!snomedCode) {
+    throw new BadRequestError(
+      "Missing SNOMED code for condition",
+      undefined,
+      { ehr: EhrSources.athena, practiceId }
+    );
+  }
 
   const secondaryMappings = await getSecondaryMappings({ … });
   // …
   await client.createProblem({
     cxId,
     patientId: ehrPatientId,
     departmentId: athenaDepartmentId,
     condition,
   });
 }

This check is purely optional but will fail fast with a descriptive error if a SNOMED code is absent.

packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (1)

639-642: getEhrGroupedVitals usage verified—please add guardrail test

I’ve confirmed that in packages/core/src/external/ehr/job/bundle/write-back-bundles:

  • There are no remaining references to the old GroupedVitalsByDate shape or isGroupedVitalsByDate.
  • All grouped‐vitals write-back paths now call getEhrGroupedVitals.

The switch is safe and EHR-aware. Please add or adjust a unit/integration test to cover both Athena (code-based) and Elation (date-based) grouping paths.

packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1)

23-34: Duplicate department resolution logic — extract a shared helper

This logic mirrors the pattern in the Athena condition write-back (and likely labs). Centralize in a single resolver to ensure consistent selection, logging, and future preferences (e.g., preferred department).

Apply this diff locally to use a helper:

-  const secondaryMappings = await getSecondaryMappings({
-    ehr: EhrSources.athena,
-    practiceId,
-    schema: athenaSecondaryMappingsSchema,
-  });
-  const athenaDepartmentId = secondaryMappings.departmentIds[0];
+  const secondaryMappings = await getSecondaryMappings({
+    ehr: EhrSources.athena,
+    practiceId,
+    schema: athenaSecondaryMappingsSchema,
+  });
+  const [athenaDepartmentId] = secondaryMappings.departmentIds ?? [];
   if (!athenaDepartmentId) {
     throw new BadRequestError("Department ID not found", undefined, {
       ehr: EhrSources.athena,
       practiceId,
     });
   }

And consider adding a shared resolver in packages/core/src/external/ehr/athenahealth/shared.ts:

// in athenahealth/shared.ts
export async function resolveAthenaDepartmentId(practiceId: string): Promise<string> {
  const mappings = await getSecondaryMappings({
    ehr: EhrSources.athena,
    practiceId,
    schema: athenaSecondaryMappingsSchema,
  });
  const [id] = mappings.departmentIds ?? [];
  if (!id) {
    throw new BadRequestError("Department ID not found", undefined, {
      ehr: EhrSources.athena,
      practiceId,
    });
  }
  return id;
}

Then replace local logic with const athenaDepartmentId = await resolveAthenaDepartmentId(practiceId);

If you want, I can prep a follow-up PR to introduce this helper and update all Athena write-backs for consistency.

📜 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 db9b73c and 0703c6f.

📒 Files selected for processing (10)
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (1 hunks)
  • packages/core/src/external/ehr/command/write-back/condition.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3 hunks)
  • packages/core/src/external/ehr/command/write-back/lab.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/shared.ts (3 hunks)
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (2 hunks)
  • packages/core/src/external/ehr/shared.ts (3 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/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
🧠 Learnings (7)
📚 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/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-08-12T17:28:06.150Z
Learnt from: thomasyopes
PR: metriport/metriport#4358
File: packages/core/src/external/ehr/command/write-back/shared.ts:21-21
Timestamp: 2025-08-12T17:28:06.150Z
Learning: In packages/core/src/external/ehr/command/write-back/shared.ts, the WriteBackResourceRequest.primaryResourceOrResources field deliberately uses the generic tuple [Date, Resource[]] instead of the more specific GroupedVitalsByDate type ([Date, Observation[]]) to keep the shared interface flexible for potentially grouping different resource types by date in the future.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
📚 Learning: 2025-08-15T00:00:45.080Z
Learnt from: keshavsaharia
PR: metriport/metriport#4370
File: packages/shared/src/domain/patient.ts:5-5
Timestamp: 2025-08-15T00:00:45.080Z
Learning: The patientSchema in packages/shared/src/domain/patient.ts is used in a limited subsystem scope ("SS") and is not the same as other Patient schemas referenced throughout the codebase, making additive optional field changes like externalId safe and non-breaking.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-03-19T13:44:54.126Z
Learnt from: leite08
PR: metriport/metriport#3463
File: packages/api/src/command/medical/patient/settings/create-patient-settings.ts:174-175
Timestamp: 2025-03-19T13:44:54.126Z
Learning: In the Metriport codebase, Lodash is used extensively and preferred for array and object manipulations. When working with batches of data, use Lodash's `_.chunk()` instead of manual array slicing with `array.slice(i, i + batchSize)`. The codebase also commonly uses other Lodash functions like `cloneDeep`, `uniq`, `uniqBy`, `groupBy`, and `orderBy`. Prefer Lodash functions over native JavaScript implementations for better readability and consistency across the codebase.

Applied to files:

  • packages/core/src/external/ehr/shared.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/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
🧬 Code graph analysis (8)
packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (5)
packages/core/src/external/ehr/command/write-back/lab.ts (2)
  • writeBackLab (18-21)
  • WriteBackLabClientRequest (16-16)
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/shared/src/index.ts (1)
  • BadRequestError (42-42)
packages/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (8-40)
packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (4)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (2)
  • WriteBackGroupedVitalsClientRequest (29-29)
  • isGroupedVitalsByCode (70-72)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (8-40)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (4)
packages/core/src/external/ehr/command/write-back/condition.ts (2)
  • writeBackCondition (19-25)
  • WriteBackConditionClientRequest (17-17)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (8-40)
packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (2)
packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1)
  • writeBackGroupedVitals (11-46)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)
  • writeBackGroupedVitals (31-37)
  • WriteBackGroupedVitalsClientRequest (29-29)
  • isGroupedVitalsByDate (66-68)
packages/core/src/external/ehr/shared.ts (2)
packages/commonwell-sdk/src/models/document.ts (1)
  • Coding (21-21)
packages/core/src/fhir-deduplication/shared.ts (2)
  • UNK_CODE (26-26)
  • UNKNOWN_DISPLAY (27-27)
packages/core/src/external/ehr/command/write-back/shared.ts (1)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)
  • EhrGroupedVitals (18-18)
packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (1)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)
  • getEhrGroupedVitals (169-185)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)
packages/core/src/external/ehr/shared.ts (8)
  • GroupedVitals (365-369)
  • GroupedVitalsByDate (371-371)
  • getObservationObservedDate (666-672)
  • formatDate (99-105)
  • GroupedObservation (373-380)
  • getValidCode (1010-1021)
  • handleTitleSpecialCases (1023-1042)
  • handleBloodPressureMapping (1044-1094)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (11-11)
packages/shared/src/common/date.ts (1)
  • buildDayjs (100-102)
⏰ 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: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
🔇 Additional comments (16)
packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (2)

8-33: Athena lab write-back wiring looks good

Flow is straightforward: fetch secondary mappings, validate department, instantiate client, submit lab result. Error contexts include ehr and practiceId. No functional issues spotted.


27-32: createLabResultDocument payload shape is correct
I verified that in packages/core/src/external/ehr/athenahealth/index.ts the client defines:

async createLabResultDocument({
  cxId,
  patientId,
  departmentId,
  observation,
}: {
  cxId: string;
  patientId: string;
  departmentId: string;
  observation: Observation;
}): Promise<CreatedLabResultSuccess> {  }

– which exactly matches the four fields you’re passing in the write-back call. No mismatches in field names or types were found.

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

368-380: Type additions for vitals grouping look appropriate

Adding title?: string, GroupedVitalsByDate, and GroupedObservation provides the necessary structure for code- and date-based paths.

packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)

169-185: LGTM: dispatching grouping strategy by EHR source

Clear separation: Athena -> by code; Elation -> by date; appropriate BadRequest on unsupported EHRs.

packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (2)

16-21: Good runtime validation for union type

Validating groupedVitals with isGroupedVitalsByDate keeps the Elation path safe from wrong payloads.


23-48: Wiring and error contexts look solid

Secondary mappings validation and client invocation are consistent with existing patterns. No issues spotted.

packages/core/src/external/ehr/command/write-back/lab.ts (1)

27-33: Athena handler successfully added to dispatch map

This enables Athena lab write-back via the shared entry point. All good.

packages/core/src/external/ehr/command/write-back/condition.ts (2)

5-5: Athena handler wiring import looks good

Alias import pattern is consistent with other write-back handlers and keeps the dispatch table clean.


31-37: Athena write-back handler correctly exported and registered

The writeBackConditionAthena function is properly exported from
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts and the dispatcher map in
packages/core/src/external/ehr/command/write-back/condition.ts includes the [EhrSources.athena]: writeBackConditionAthena entry. No other ehrWriteBackConditionMap definitions were found, and there are no stale or missing mappings. All checks out—approving this change.

packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1)

22-26: Client creation pattern is consistent

Optional token propagation via spread is clean and aligned with our conventions. No action needed.

packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (1)

33-35: Importing getEhrGroupedVitals is the right abstraction

Centralizing grouping per EHR in one place reduces drift with future EHRs and keeps this job focused on orchestration.

packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (2)

16-21: Good runtime validation for grouped-vitals shape

Type guard ensures Athena receives code-grouped vitals only. Error metadata is helpful for triage.


35-45: Client call parameters are correct and consistent with our createVitals contract

Passing cxId, patientId, departmentId, and the grouped vitals payload matches the established pattern.

packages/core/src/external/ehr/command/write-back/shared.ts (3)

5-5: Importing EhrGroupedVitals keeps the public API flexible

Moving to a union type prepares us for multiple grouping strategies without leaking EHR specifics here.


17-17: Type broadening for primaryResourceOrResources is appropriate

Allowing EhrGroupedVitals here removes the need for local shape checks. Validation remains in EHR-specific handlers.


42-45: No lingering raw tuple usage in grouped-vitals dispatch—resolved

All call sites have been updated to forward the EhrGroupedVitals type. I verified across the repository that:

  • There are no remaining writeBackResource({…, writeBackResource: "grouped-vitals" }) calls.
  • writeBackGroupedVitals is only defined and invoked in the elation and athenahealth modules.
  • No raw [Date, Observation[]] tuples remain anywhere in packages/core/src.

No further changes are required.

Base automatically changed from eng-559-elation-writeback-followups to develop August 27, 2025 21:06
@alexeysergeev-cm alexeysergeev-cm force-pushed the eng-585-athena-auto-writeback branch from 0703c6f to 55293b7 Compare August 28, 2025 20:42
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: 3

♻️ Duplicate comments (2)
packages/core/src/external/ehr/shared.ts (1)

1087-1136: BP pairing loses data and is timestamp-fragile; normalize keys and preserve singletons

Pairs are done by raw date string; near-equal timestamps won’t match. Non S/D BP entries are dropped. Reuse the normalized-key strategy and keep unmatched entries.

Apply:

 export function handleBloodPressureMapping(obsMap: Map<string, GroupedObservation[]>) {
   const bloodPressure = obsMap.get("Blood Pressure");
   if (!bloodPressure) return;

-  const groupedBloodPressure: GroupedObservation[] = [];
-
-  const systolicMap = new Map<string, number>();
-  const diastolicMap = new Map<string, number>();
-
-  bloodPressure.forEach(bp => {
-    if (bp.grouping?.toLowerCase().includes("systolic")) {
-      systolicMap.set(bp.date, bp.observation);
-    } else if (bp.grouping?.toLowerCase().includes("diastolic")) {
-      diastolicMap.set(bp.date, bp.observation);
-    }
-  });
-
-  bloodPressure.forEach(bp => {
-    if (bp.grouping?.toLowerCase().includes("systolic")) {
-      const diastolicValue = diastolicMap.get(bp.date);
-      if (diastolicValue !== undefined) {
-        groupedBloodPressure.push({
-          ...bp,
-          bp: {
-            systolic: bp.observation,
-            diastolic: diastolicValue,
-          },
-        });
-      }
-    } else if (bp.grouping?.toLowerCase().includes("diastolic")) {
-      const systolicValue = systolicMap.get(bp.date);
-      if (systolicValue !== undefined) {
-        if (
-          !groupedBloodPressure.some(
-            gbp => gbp.date === bp.date && gbp.bp?.diastolic === bp.observation
-          )
-        ) {
-          groupedBloodPressure.push({
-            ...bp,
-            bp: {
-              systolic: systolicValue,
-              diastolic: bp.observation,
-            },
-          });
-        }
-      }
-    }
-  });
-
-  obsMap.set("Blood Pressure", groupedBloodPressure);
+  const groupedBloodPressure: GroupedObservation[] = [];
+  const systolicMap = new Map<string, number>();
+  const diastolicMap = new Map<string, number>();
+
+  const toKey = (d: string) => buildDayjs(d).toISOString();
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const key = toKey(bp.date);
+    if (lower.includes("systolic")) {
+      systolicMap.set(key, bp.observation);
+    } else if (lower.includes("diastolic")) {
+      diastolicMap.set(key, bp.observation);
+    }
+  });
+
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const key = toKey(bp.date);
+    if (lower.includes("systolic")) {
+      const diastolicValue = diastolicMap.get(key);
+      if (diastolicValue !== undefined) {
+        const candidate = { ...bp, bp: { systolic: bp.observation, diastolic: diastolicValue } };
+        const dup = groupedBloodPressure.some(
+          gbp =>
+            toKey(gbp.date) === key &&
+            gbp.bp?.systolic === candidate.bp!.systolic &&
+            gbp.bp?.diastolic === candidate.bp!.diastolic
+        );
+        if (!dup) groupedBloodPressure.push(candidate);
+      }
+    } else if (lower.includes("diastolic")) {
+      const systolicValue = systolicMap.get(key);
+      if (systolicValue !== undefined) {
+        const candidate = { ...bp, bp: { systolic: systolicValue, diastolic: bp.observation } };
+        const dup = groupedBloodPressure.some(
+          gbp =>
+            toKey(gbp.date) === key &&
+            gbp.bp?.systolic === candidate.bp!.systolic &&
+            gbp.bp?.diastolic === candidate.bp!.diastolic
+        );
+        if (!dup) groupedBloodPressure.push(candidate);
+      }
+    }
+  });
+
+  // Preserve non S/D BP observations and unpaired singletons
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const key = toKey(bp.date);
+    const isSD = lower.includes("systolic") || lower.includes("diastolic");
+    const hasPair = groupedBloodPressure.some(gbp => toKey(gbp.date) === key);
+    if (!isSD || !hasPair) groupedBloodPressure.push(bp);
+  });
+
+  obsMap.set("Blood Pressure", groupedBloodPressure);
 }
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)

98-167: Bug: numeric parsing drops decimals and treats 0 as missing

  • parseInt truncates decimals (e.g., 98.6 → 98).
  • if (!date || !observationValue) discards valid zero values.

Apply:

-    const observationValue = v.valueQuantity?.value ?? v.valueString;
+    const observationValue = v.valueQuantity?.value ?? v.valueString;
     const date = v.effectiveDateTime ?? v.effectivePeriod?.start;
-    if (!date || !observationValue) {
+    if (!date || observationValue == null) {
       results.push({ title, mostRecentObservation: v });
       return;
     }
 
     const observationUnit = v.valueQuantity?.unit?.replace(/[{()}]/g, "");
-    const observationPoint: GroupedObservation = {
-      rawVital: v,
-      date,
-      observation:
-        typeof observationValue === "number" ? observationValue : parseInt(observationValue),
-      ...(observationUnit && { unit: observationUnit }),
-    };
+    const parsedValue =
+      typeof observationValue === "number"
+        ? observationValue
+        : parseFloat(String(observationValue));
+    if (!Number.isFinite(parsedValue)) {
+      results.push({ title, mostRecentObservation: v });
+      return;
+    }
+    const observationPoint: GroupedObservation = {
+      rawVital: v,
+      date,
+      observation: parsedValue,
+      ...(observationUnit && { unit: observationUnit }),
+    };
🧹 Nitpick comments (6)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (2)

15-21: Don’t pick the first department arbitrarily.

Selecting departmentIds[0] is non-deterministic across envs. Prefer an explicit selection strategy (e.g., configured default write-back department, per-resource override, or stable sort by ID) and log which department is used.

I can wire a selectDepartmentId(secondaryMappings) helper with clear precedence rules.


27-33: Add observability around the write-back.

Log at debug level before/after createProblem with cxId, practiceId, patientId, and departmentId (no PHI). This eases triage without noisy multi-line logs.

packages/core/src/external/ehr/shared.ts (2)

413-422: Clarify GroupedObservation.date semantics

Define whether date must be ISO 8601 (full datetime) or date-only. A branded type alias (e.g., type IsoDateString = string & { __isoDate: true }) or a short doc comment reduces accidental mixing.


1053-1065: getValidCode is fine; consider broader acceptance in future

Current filter requires both code and display (not UNK/unknown). If some EHRs omit display but have valid code, you might want to keep those and fall back to code. Not blocking.

packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (2)

74-96: Group-by-date: consider stable output ordering

Optional: sort the returned array by date for deterministic behavior (useful for tests/logs).

-  return Object.entries(groupedVitals).map(([chartDate, observations]) => [
-    buildDayjs(chartDate).toDate(),
-    observations,
-  ]);
+  return Object.entries(groupedVitals)
+    .sort(([a], [b]) => buildDayjs(a).diff(buildDayjs(b)))
+    .map(([chartDate, observations]) => [buildDayjs(chartDate).toDate(), observations]);

169-185: Dispatch by EHR — good; add future-proofing note

Throwing on unknown EHR is fine. If more EHRs are added, extend writeBackGroupedVitalsEhrs consistently.

📜 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 0703c6f and 55293b7.

📒 Files selected for processing (10)
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (1 hunks)
  • packages/core/src/external/ehr/command/write-back/condition.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3 hunks)
  • packages/core/src/external/ehr/command/write-back/lab.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/shared.ts (3 hunks)
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (2 hunks)
  • packages/core/src/external/ehr/shared.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
🧰 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/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.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/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
🧠 Learnings (8)
📚 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/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-08-12T17:28:06.150Z
Learnt from: thomasyopes
PR: metriport/metriport#4358
File: packages/core/src/external/ehr/command/write-back/shared.ts:21-21
Timestamp: 2025-08-12T17:28:06.150Z
Learning: In packages/core/src/external/ehr/command/write-back/shared.ts, the WriteBackResourceRequest.primaryResourceOrResources field deliberately uses the generic tuple [Date, Resource[]] instead of the more specific GroupedVitalsByDate type ([Date, Observation[]]) to keep the shared interface flexible for potentially grouping different resource types by date in the future.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-08-25T17:29:25.387Z
Learnt from: thomasyopes
PR: metriport/metriport#4451
File: packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts:143-151
Timestamp: 2025-08-25T17:29:25.387Z
Learning: In packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts, the filterObservations function with vital.latestOnly=true is designed to keep only the most recent observation per LOINC code across all dates, not per date+LOINC pair. This behavior intentionally collapses multiple days into a single latest reading per code.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["NORMAL", "ABNORMAL", "CRITICAL", "UNKNOWN"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["high", "low", "normal"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/shared.ts
📚 Learning: 2025-03-19T13:44:54.126Z
Learnt from: leite08
PR: metriport/metriport#3463
File: packages/api/src/command/medical/patient/settings/create-patient-settings.ts:174-175
Timestamp: 2025-03-19T13:44:54.126Z
Learning: In the Metriport codebase, Lodash is used extensively and preferred for array and object manipulations. When working with batches of data, use Lodash's `_.chunk()` instead of manual array slicing with `array.slice(i, i + batchSize)`. The codebase also commonly uses other Lodash functions like `cloneDeep`, `uniq`, `uniqBy`, `groupBy`, and `orderBy`. Prefer Lodash functions over native JavaScript implementations for better readability and consistency across the codebase.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
🧬 Code graph analysis (3)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (4)
packages/core/src/external/ehr/command/write-back/condition.ts (2)
  • writeBackCondition (19-25)
  • WriteBackConditionClientRequest (17-17)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (7-36)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)
packages/core/src/external/ehr/shared.ts (8)
  • GroupedVitals (407-411)
  • GroupedVitalsByDate (413-413)
  • getObservationObservedDate (708-714)
  • formatDate (105-111)
  • GroupedObservation (415-422)
  • getValidCode (1053-1064)
  • handleTitleSpecialCases (1066-1085)
  • handleBloodPressureMapping (1087-1137)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (11-11)
packages/shared/src/common/date.ts (1)
  • buildDayjs (100-102)
packages/core/src/external/ehr/shared.ts (2)
packages/commonwell-sdk/src/models/document.ts (1)
  • Coding (21-21)
packages/core/src/fhir-deduplication/shared.ts (2)
  • UNK_CODE (26-26)
  • UNKNOWN_DISPLAY (27-27)
⏰ 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: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (2)

10-14: Propagate secondary-mapping fetch context on failure paths. getSecondaryMappings already wraps errors; you’re fine here. If you add more logic later, preserve the original error via cause to meet guidelines.


27-32: 俄

packages/core/src/external/ehr/shared.ts (2)

52-56: LGTM on new imports

Importing UNK_CODE/UNKNOWN_DISPLAY and fetchCodingCodeOrDisplayOrSystem here centralizes code validation well.


410-412: Add title on GroupedVitals — good

Optional title improves UX for grouped vitals.

packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (4)

5-6: Athena handler wiring and EHR allowlist — good

Handler map and writeBackGroupedVitalsEhrs correctly include Athena and Elation.

Also applies to: 43-49, 61-61


66-73: Type guards look correct

isGroupedVitalsByDate and isGroupedVitalsByCode are sufficient for runtime branching.


145-147: BP mapping side-effects are applied — ensure function robustness

This relies on handleBloodPressureMapping correctness. See comments in shared.ts to normalize timestamps and preserve singletons.


18-27: Handlers enforce each variant before use—union type safe. Both Athena and Elation implementations guard with isGroupedVitalsByCode or isGroupedVitalsByDate before proceeding.

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: 0

♻️ Duplicate comments (2)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1)

9-10: Token auth fix acknowledged; looks correct now.

Destructuring tokenInfo and passing it to createAthenaHealthClient addresses the earlier tokenId issue. ✅

Also applies to: 22-26

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

1079-1129: BP pairing is fragile and drops data; normalize timestamps and preserve singletons.

Raw string date keys can miss S/D pairing when timestamps differ slightly; non S/D BP entries are dropped.

Apply:

 export function handleBloodPressureMapping(obsMap: Map<string, GroupedObservation[]>) {
   const bloodPressure = obsMap.get("Blood Pressure");
   if (!bloodPressure) return;

   const groupedBloodPressure: GroupedObservation[] = [];

-  const systolicMap = new Map<string, number>();
-  const diastolicMap = new Map<string, number>();
+  const systolicMap = new Map<string, number>();
+  const diastolicMap = new Map<string, number>();

-  bloodPressure.forEach(bp => {
-    if (bp.grouping?.toLowerCase().includes("systolic")) {
-      systolicMap.set(bp.date, bp.observation);
-    } else if (bp.grouping?.toLowerCase().includes("diastolic")) {
-      diastolicMap.set(bp.date, bp.observation);
-    }
-  });
+  const toKey = (d: string) => buildDayjs(d).toISOString();
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const key = toKey(bp.date);
+    if (lower.includes("systolic")) {
+      systolicMap.set(key, bp.observation);
+    } else if (lower.includes("diastolic")) {
+      diastolicMap.set(key, bp.observation);
+    }
+  });

-  bloodPressure.forEach(bp => {
-    if (bp.grouping?.toLowerCase().includes("systolic")) {
-      const diastolicValue = diastolicMap.get(bp.date);
-      if (diastolicValue !== undefined) {
-        groupedBloodPressure.push({
-          ...bp,
-          bp: {
-            systolic: bp.observation,
-            diastolic: diastolicValue,
-          },
-        });
-      }
-    } else if (bp.grouping?.toLowerCase().includes("diastolic")) {
-      const systolicValue = systolicMap.get(bp.date);
-      if (systolicValue !== undefined) {
-        if (
-          !groupedBloodPressure.some(
-            gbp => gbp.date === bp.date && gbp.bp?.diastolic === bp.observation
-          )
-        ) {
-          groupedBloodPressure.push({
-            ...bp,
-            bp: {
-              systolic: systolicValue,
-              diastolic: bp.observation,
-            },
-          });
-        }
-      }
-    }
-  });
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const key = toKey(bp.date);
+    if (lower.includes("systolic")) {
+      const diastolicValue = diastolicMap.get(key);
+      if (diastolicValue !== undefined) {
+        const candidate = { ...bp, bp: { systolic: bp.observation, diastolic: diastolicValue } };
+        const dup = groupedBloodPressure.some(
+          gbp =>
+            toKey(gbp.date) === key &&
+            gbp.bp?.systolic === candidate.bp!.systolic &&
+            gbp.bp?.diastolic === candidate.bp!.diastolic
+        );
+        if (!dup) groupedBloodPressure.push(candidate);
+      }
+    } else if (lower.includes("diastolic")) {
+      const systolicValue = systolicMap.get(key);
+      if (systolicValue !== undefined) {
+        const candidate = { ...bp, bp: { systolic: systolicValue, diastolic: bp.observation } };
+        const dup = groupedBloodPressure.some(
+          gbp =>
+            toKey(gbp.date) === key &&
+            gbp.bp?.systolic === candidate.bp!.systolic &&
+            gbp.bp?.diastolic === candidate.bp!.diastolic
+        );
+        if (!dup) groupedBloodPressure.push(candidate);
+      }
+    }
+  });

-  obsMap.set("Blood Pressure", groupedBloodPressure);
+  // Preserve non S/D BP observations and any singletons that couldn't be paired
+  bloodPressure.forEach(bp => {
+    const lower = bp.grouping?.toLowerCase() ?? "";
+    const isSD = lower.includes("systolic") || lower.includes("diastolic");
+    const hasPair = groupedBloodPressure.some(gbp => toKey(gbp.date) === toKey(bp.date));
+    if (!isSD || !hasPair) groupedBloodPressure.push(bp);
+  });
+
+  obsMap.set("Blood Pressure", groupedBloodPressure);
 }
🧹 Nitpick comments (3)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1)

17-20: Add write-back context to error metadata.

Include writeBackResource: "condition" in BadRequestError additionalInfo to improve observability.

Apply:

   if (!athenaDepartmentId) {
-    throw new BadRequestError("Department ID not found", undefined, {
-      ehr: EhrSources.athena,
-      practiceId,
-    });
+    throw new BadRequestError("Department ID not found", undefined, {
+      ehr: EhrSources.athena,
+      practiceId,
+      writeBackResource: "condition",
+    });
   }
packages/core/src/external/ehr/shared.ts (1)

1066-1077: Broaden BMI match; keep case handling consistent.

Match “body mass index” too and avoid mixing lower with a case-insensitive regex target.

Apply:

 export function handleTitleSpecialCases(
   title: string,
   observationPoint: GroupedObservation
 ): string {
-  const lower = title.toLowerCase();
-  if (/(blood pressure|bp sys|bp dias|systolic|diastolic)/i.test(title)) {
+  const lower = title.toLowerCase();
+  if (/(blood pressure|bp sys|bp dias|systolic|diastolic)/i.test(lower)) {
     observationPoint.grouping = title;
     return "Blood Pressure";
   }
-  if (lower.includes("bmi")) return "Body Mass Index (BMI)";
+  if (/(bmi|body\s*mass\s*index)/i.test(lower)) return "Body Mass Index (BMI)";
   return title;
 }
packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1)

35-45: Add write-back context to error metadata (consistency).

If department is missing, include writeBackResource: "grouped-vitals" for easier triage; otherwise the flow looks good.

Apply:

   if (!athenaDepartmentId) {
-    throw new BadRequestError("Department ID not found", undefined, {
-      ehr: EhrSources.athena,
-      practiceId,
-    });
+    throw new BadRequestError("Department ID not found", undefined, {
+      ehr: EhrSources.athena,
+      practiceId,
+      writeBackResource: "grouped-vitals",
+    });
   }
📜 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 55293b7 and 786ae4b.

📒 Files selected for processing (4)
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (1 hunks)
  • packages/core/src/external/ehr/shared.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
🧰 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/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.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/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
🧠 Learnings (11)
📚 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/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
📚 Learning: 2025-08-12T17:28:06.150Z
Learnt from: thomasyopes
PR: metriport/metriport#4358
File: packages/core/src/external/ehr/command/write-back/shared.ts:21-21
Timestamp: 2025-08-12T17:28:06.150Z
Learning: In packages/core/src/external/ehr/command/write-back/shared.ts, the WriteBackResourceRequest.primaryResourceOrResources field deliberately uses the generic tuple [Date, Resource[]] instead of the more specific GroupedVitalsByDate type ([Date, Observation[]]) to keep the shared interface flexible for potentially grouping different resource types by date in the future.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.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/core/src/external/ehr/athenahealth/command/write-back/condition.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/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-08-27T23:13:51.351Z
Learnt from: thomasyopes
PR: metriport/metriport#4478
File: packages/core/src/external/ehr/command/write-back/grouped-vitals.ts:10-10
Timestamp: 2025-08-27T23:13:51.351Z
Learning: In `packages/lambdas/src/shared/ehr.ts`, the `tokenId` field in `ehrCreateResourceDiffBundlesSchema` is intentionally kept as legacy support with a TODO comment referencing ENG-937, allowing existing queue messages to be processed during the tokenId → tokenInfo migration. This is not a missing update but intentional technical debt to be cleaned up once all queue messages have been processed.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
📚 Learning: 2025-06-01T13:48:26.600Z
Learnt from: leite08
PR: metriport/metriport#3942
File: packages/core/src/external/fhir/__tests__/location.ts:6-9
Timestamp: 2025-06-01T13:48:26.600Z
Learning: In TypeScript test code, don't flag deconstructed object parameters as needing simplification or refactoring. Test helper functions with nested optional object parameters (like `param?: { patient?: PatientWithId; location?: Partial<Location>; }`) are acceptable and should not be suggested for flattening.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
📚 Learning: 2025-07-16T00:54:56.156Z
Learnt from: leite08
PR: metriport/metriport#4193
File: packages/carequality-sdk/src/client/carequality-fhir.ts:198-208
Timestamp: 2025-07-16T00:54:56.156Z
Learning: User leite08 accepted the risk of parameter mutation in packages/carequality-sdk/src/client/carequality-fhir.ts when modifying org.active = false directly on input parameters, choosing to proceed despite the violation of the "Avoid modifying objects received as parameter" coding guideline.

Applied to files:

  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
📚 Learning: 2025-08-25T17:29:25.387Z
Learnt from: thomasyopes
PR: metriport/metriport#4451
File: packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts:143-151
Timestamp: 2025-08-25T17:29:25.387Z
Learning: In packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts, the filterObservations function with vital.latestOnly=true is designed to keep only the most recent observation per LOINC code across all dates, not per date+LOINC pair. This behavior intentionally collapses multiple days into a single latest reading per code.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["NORMAL", "ABNORMAL", "CRITICAL", "UNKNOWN"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["high", "low", "normal"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
📚 Learning: 2025-03-19T13:44:54.126Z
Learnt from: leite08
PR: metriport/metriport#3463
File: packages/api/src/command/medical/patient/settings/create-patient-settings.ts:174-175
Timestamp: 2025-03-19T13:44:54.126Z
Learning: In the Metriport codebase, Lodash is used extensively and preferred for array and object manipulations. When working with batches of data, use Lodash's `_.chunk()` instead of manual array slicing with `array.slice(i, i + batchSize)`. The codebase also commonly uses other Lodash functions like `cloneDeep`, `uniq`, `uniqBy`, `groupBy`, and `orderBy`. Prefer Lodash functions over native JavaScript implementations for better readability and consistency across the codebase.

Applied to files:

  • packages/core/src/external/ehr/shared.ts
🧬 Code graph analysis (3)
packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (5)
packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (1)
  • writeBackGroupedVitals (11-48)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)
  • writeBackGroupedVitals (31-37)
  • WriteBackGroupedVitalsClientRequest (29-29)
  • isGroupedVitalsByCode (70-72)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (7-36)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (4)
packages/core/src/external/ehr/command/write-back/condition.ts (2)
  • writeBackCondition (19-25)
  • WriteBackConditionClientRequest (17-17)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (7-36)
packages/core/src/external/ehr/shared.ts (2)
packages/commonwell-sdk/src/models/document.ts (1)
  • Coding (21-21)
packages/core/src/fhir-deduplication/shared.ts (2)
  • UNK_CODE (26-26)
  • UNKNOWN_DISPLAY (27-27)
⏰ 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: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (2)

15-21: Department selection: confirm “first ID” is intended.

Using secondaryMappings.departmentIds[0] may pick a non-default department. Verify mapping config guarantees index 0 is the intended write-back target, or select by an explicit default key.


27-32: Athena condition status support: validate/normalize before send.

Per prior learnings, only “relapse”/“recurrence” map reliably to Athena problem status. Ensure condition’s status is acceptable or normalized before createProblem, otherwise Athena may reject or misclassify.

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

52-56: Import updates look correct.

Import path to ../../fhir-deduplication/shared is accurate from this module; using UNK_CODE/UNKNOWN_DISPLAY aligns with the new filtering util.


407-422: Grouped types addition LGTM.

title? on GroupedVitals, GroupedVitalsByDate, and GroupedObservation shape are coherent with the grouped-vitals flow.


1053-1064: Filter unknown codes/display: edge-safe and fine.

Logic correctly guards missing fields and trims case-insensitively. No further changes needed.

packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (2)

14-21: Type guard on groupedVitals is appropriate.

Using isGroupedVitalsByCode ensures Athena path receives code-grouped payloads.


28-34: Department selection: confirm first-id assumption.

Same concern as conditions path—ensure [0] is the intended default for vitals write-back.

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: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/core/src/external/ehr/elation/index.ts (1)

1178-1182: Use strict equality per TS guidelines.

Follow repo standards: prefer ===/!== over ==/!=.

-      if (!resultStatus || resultStatus.trim().toLowerCase() != "final") {
+      if (!resultStatus || resultStatus.trim().toLowerCase() !== "final") {
🧹 Nitpick comments (2)
packages/core/src/external/ehr/elation/index.ts (2)

624-629: Tighten flatMap result type.

Return an array consistently to avoid union typing and improve readability.

-    const vitals = observations.flatMap(observation => {
-      const formattedVital = this.formatGroupedVital(observation);
-      if (!formattedVital) return [];
-      return formattedVital;
-    });
+    const vitals = observations.flatMap(observation => {
+      const formattedVital = this.formatGroupedVital(observation);
+      return formattedVital ? [formattedVital] : [];
+    });

533-534: Typo in additionalInfo key: diagnostricReportId → diagnosticReportId.

Fix the log/metadata field name.

-      diagnostricReportId: diagnosticReport.id,
+      diagnosticReportId: diagnosticReport.id,
📜 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 786ae4b and 0a0abfe.

📒 Files selected for processing (1)
  • packages/core/src/external/ehr/elation/index.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/elation/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/elation/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/elation/index.ts
🧠 Learnings (1)
📚 Learning: 2025-08-12T17:28:06.150Z
Learnt from: thomasyopes
PR: metriport/metriport#4358
File: packages/core/src/external/ehr/command/write-back/shared.ts:21-21
Timestamp: 2025-08-12T17:28:06.150Z
Learning: In packages/core/src/external/ehr/command/write-back/shared.ts, the WriteBackResourceRequest.primaryResourceOrResources field deliberately uses the generic tuple [Date, Resource[]] instead of the more specific GroupedVitalsByDate type ([Date, Observation[]]) to keep the shared interface flexible for potentially grouping different resource types by date in the future.

Applied to files:

  • packages/core/src/external/ehr/elation/index.ts
⏰ 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: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/core/src/external/ehr/elation/index.ts (2)

81-81: LGTM: import moved to shared aggregator.

Importing GroupedVitalsByDate from ../shared keeps the type centralized and aligns with the recent shared abstractions.


81-81: GroupedVitalsByDate export and imports verified
GroupedVitalsByDate is exported from packages/core/src/external/ehr/shared.ts (line 413) and all usages (e.g. in elation/index.ts) import it from ../shared; no stale imports remain.

metriport/metriport-internal#1040

Ref:
Signed-off-by: Alexey Sergeev <alexey@metriport.com>
metriport/metriport-internal#1040

Ref:
Signed-off-by: Alexey Sergeev <alexey@metriport.com>
metriport/metriport-internal#1040

Ref:
Signed-off-by: Alexey Sergeev <alexey@metriport.com>
@alexeysergeev-cm alexeysergeev-cm force-pushed the eng-585-athena-auto-writeback branch from 0a0abfe to f84b52e Compare August 29, 2025 21:13
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: 1

♻️ Duplicate comments (1)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)

118-133: Bug: drops decimals and treats 0 as missing; also misses end-date fallback

Replace parseInt with parseFloat and use explicit null checks; use getObservationObservedDate for full fallback.

Apply:

-    const observationValue = v.valueQuantity?.value ?? v.valueString;
-    const date = v.effectiveDateTime ?? v.effectivePeriod?.start;
-    if (!date || !observationValue) {
+    const observationValue = v.valueQuantity?.value ?? v.valueString;
+    const date = getObservationObservedDate(v);
+    if (!date || observationValue == null) {
       results.push({ title, mostRecentObservation: v });
       return;
     }
 
-    const observationUnit = v.valueQuantity?.unit?.replace(/[{()}]/g, "");
-    const observationPoint: GroupedObservation = {
-      rawVital: v,
-      date,
-      observation:
-        typeof observationValue === "number" ? observationValue : parseInt(observationValue),
-      ...(observationUnit && { unit: observationUnit }),
-    };
+    const observationUnit = v.valueQuantity?.unit?.replace(/[{()}]/g, "")?.trim();
+    const parsedValue =
+      typeof observationValue === "number"
+        ? observationValue
+        : parseFloat(String(observationValue));
+    if (!Number.isFinite(parsedValue)) {
+      results.push({ title, mostRecentObservation: v });
+      return;
+    }
+    const observationPoint: GroupedObservation = {
+      rawVital: v,
+      date,
+      observation: parsedValue,
+      ...(observationUnit && { unit: observationUnit }),
+    };
🧹 Nitpick comments (7)
packages/core/src/external/ehr/command/write-back/condition.ts (1)

5-6: Wrap long imports to respect the 100-char max line length.
packages/core/src/external/ehr/command/write-back/condition.ts:5–6

-import { writeBackCondition as writeBackConditionAthena } from "../../athenahealth/command/write-back/condition";
+import {
+  writeBackCondition as writeBackConditionAthena,
+} from "../../athenahealth/command/write-back/condition";

-import { writeBackCondition as writeBackConditionElation } from "../../elation/command/write-back/condition";
+import {
+  writeBackCondition as writeBackConditionElation,
+} from "../../elation/command/write-back/condition";
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (4)

105-144: Prefer forEach/for..of over map for side-effects

map’s return is unused here; forEach or a simple for..of is clearer.

Apply:

-  observations.map(v => {
+  observations.forEach(v => {
     // ...
-  });
+  });

125-132: Tiny robustness: trim unit and keep only printable ASCII

Some EHRs include stray whitespace or braces in units. You’re already stripping braces; trimming helps too (included in the earlier diff). Optionally, normalize multiple spaces.


98-143: Potential conflation: grouping key is display title, not LOINC

Different LOINC codes can share identical display text, merging unrelated series under one title. Consider grouping by LOINC when present and using the title just for labeling.

Would you like me to draft a minimal change that uses a grouping key of ${title}::${loincCode} when a LOINC is available while keeping title unchanged in the returned payload?

Also applies to: 147-165


35-37: Minor: avoid return await

Not harmful, but return handler({ ...params }) is equivalent and avoids an extra tick.

-  return await handler({ ...params });
+  return handler({ ...params });
packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (2)

15-15: Avoid implicit “first department” selection

If multiple departmentIds are configured, defaulting to index 0 may attach to the wrong department. Prefer an explicit default or document the convention inline for maintainers.

Minimal clarification:

-  const athenaDepartmentId = secondaryMappings.departmentIds[0];
+  const { departmentIds } = secondaryMappings;
+  // NOTE: if multiple departmentIds are configured, we default to the first by convention.
+  const athenaDepartmentId = departmentIds[0];

16-21: Enrich error context for triage

Include configured departmentIds in the error’s additionalInfo to speed up support/debugging.

-    throw new BadRequestError("Department ID not found", undefined, {
-      ehr: EhrSources.athena,
-      practiceId,
-    });
+    throw new BadRequestError("Department ID not found", undefined, {
+      ehr: EhrSources.athena,
+      practiceId,
+      departmentIds: secondaryMappings.departmentIds,
+    });
📜 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 0a0abfe and f84b52e.

📒 Files selected for processing (11)
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (1 hunks)
  • packages/core/src/external/ehr/command/write-back/condition.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3 hunks)
  • packages/core/src/external/ehr/command/write-back/lab.ts (2 hunks)
  • packages/core/src/external/ehr/command/write-back/shared.ts (3 hunks)
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts (1 hunks)
  • packages/core/src/external/ehr/elation/index.ts (1 hunks)
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (2 hunks)
  • packages/core/src/external/ehr/shared.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/core/src/external/ehr/elation/index.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/elation/command/write-back/grouped-vitals.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/lab.ts
  • packages/core/src/external/ehr/command/write-back/shared.ts
🧰 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/command/write-back/condition.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/command/write-back/condition.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
🧠 Learnings (7)
📚 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/command/write-back/condition.ts
  • packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts
  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/command/write-back/condition.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-08-25T17:29:25.387Z
Learnt from: thomasyopes
PR: metriport/metriport#4451
File: packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts:143-151
Timestamp: 2025-08-25T17:29:25.387Z
Learning: In packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts, the filterObservations function with vital.latestOnly=true is designed to keep only the most recent observation per LOINC code across all dates, not per date+LOINC pair. This behavior intentionally collapses multiple days into a single latest reading per code.

Applied to files:

  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-08-12T17:28:06.150Z
Learnt from: thomasyopes
PR: metriport/metriport#4358
File: packages/core/src/external/ehr/command/write-back/shared.ts:21-21
Timestamp: 2025-08-12T17:28:06.150Z
Learning: In packages/core/src/external/ehr/command/write-back/shared.ts, the WriteBackResourceRequest.primaryResourceOrResources field deliberately uses the generic tuple [Date, Resource[]] instead of the more specific GroupedVitalsByDate type ([Date, Observation[]]) to keep the shared interface flexible for potentially grouping different resource types by date in the future.

Applied to files:

  • packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
  • packages/core/src/external/ehr/command/write-back/grouped-vitals.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/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["NORMAL", "ABNORMAL", "CRITICAL", "UNKNOWN"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
📚 Learning: 2025-07-09T17:18:28.920Z
Learnt from: thomasyopes
PR: metriport/metriport#4164
File: packages/core/src/external/ehr/healthie/index.ts:1031-1076
Timestamp: 2025-07-09T17:18:28.920Z
Learning: In packages/core/src/external/ehr/healthie/index.ts, the `interpretation` field in `labObservationResult` is defined as a required enum in the Zod schema (`z.enum(["high", "low", "normal"])`), making null-safety checks unnecessary when calling `toTitleCase(labObservationResult.interpretation)`.

Applied to files:

  • packages/core/src/external/ehr/command/write-back/grouped-vitals.ts
🧬 Code graph analysis (3)
packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (5)
packages/core/src/external/ehr/command/write-back/lab.ts (2)
  • writeBackLab (18-21)
  • WriteBackLabClientRequest (16-16)
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/core/src/external/ehr/athenahealth/shared.ts (1)
  • createAthenaHealthClient (7-36)
packages/api/src/external/ehr/athenahealth/command/write-back/lab.ts (1)
  • writeLabToChart (5-26)
packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (1)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)
  • getEhrGroupedVitals (169-185)
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)
packages/core/src/external/ehr/shared.ts (8)
  • GroupedVitals (407-411)
  • GroupedVitalsByDate (413-413)
  • getObservationObservedDate (708-714)
  • formatDate (105-111)
  • GroupedObservation (415-422)
  • getValidCode (1053-1064)
  • handleTitleSpecialCases (1066-1077)
  • handleBloodPressureMapping (1079-1129)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (11-11)
packages/shared/src/common/date.ts (1)
  • buildDayjs (100-102)
⏰ 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: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
packages/core/src/external/ehr/command/write-back/condition.ts (1)

33-35: Athena handler wiring — looks good; verify status mapping coverage.

Dispatch registration is correct and consistent with the primary-code map. Please verify the Athena handler behavior for Condition.clinicalStatus beyond “relapse/recurrence” (per prior learning, others weren’t mapped). Add a test to ensure unsupported statuses are either mapped/normalized or rejected with a clear error.

packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (2)

36-38: Good import: centralized, EHR-aware grouping

Switching to getEhrGroupedVitals consolidates grouping logic and avoids local duplication.


654-657: PrimaryResourceOrResources already supports EhrGroupedVitals The WriteBackResourceRequest.primaryResourceOrResources union includes EhrGroupedVitals, so dispatch is safe.

packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (3)

45-47: Athena write-back handler wired

Adding Athena to ehrWriteBackGroupedVitalsMap looks correct.


61-64: Exported EHR list aligns with map

writeBackGroupedVitalsEhrs matches the handlers (Athena, Elation).


74-96: Date-grouping uses shared helpers and UTC—LGTM

Using formatDate + buildDayjs ensures consistent UTC dates.

packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts (2)

8-14: LGTM: clear Athena lab write-back flow

Straightforward mapping -> validate -> client -> submit, aligns with the write-back contract and style guidelines (async/await, types, no nulls).


27-33: Retry exists in client — confirm axios timeouts & add idempotency

  • The Athena client already uses the shared makeRequest which wraps axios.request with executeWithRetries (retries applied). (packages/core/src/external/ehr/shared.ts → makeRequest; packages/core/src/external/ehr/athenahealth/index.ts → createLabResultDocument / private makeRequest)
  • Confirm the axios timeout configured for the Athena client (where the axios instance is created); if no sensible response/connection timeout is set, add one (must be < surrounding lambda timeout) or explicitly set retry-on-timeout policy. (packages/core/src/external/ehr/athenahealth/index.ts)
  • Add idempotency/dedup for lab write-backs (e.g., stable dedupe key = hash(cxId + patientId + observation.id) persisted or sent as an idempotency token) to avoid duplicate lab results when jobs/webhooks are retried. (call site: packages/core/src/external/ehr/athenahealth/command/write-back/lab.ts)

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: 0

🧹 Nitpick comments (4)
packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts (4)

4-6: Prefer a more explicit constant name than “column”.

Improves grep-ability and avoids ambiguity when multiple columns are involved.

-const column = "secondary_mappings";
+const secondaryMappingsColumn = "secondary_mappings";
@@
-      tableName,
-      column,
+      tableName,
+      secondaryMappingsColumn,
@@
-    await queryInterface.removeColumn(tableName, column, { transaction });
+    await queryInterface.removeColumn(tableName, secondaryMappingsColumn, { transaction });

Also applies to: 11-12, 20-20


7-16: Up migration is safe and transactional. Consider adding a DB-level column comment.

Nullable JSONB with no default avoids table rewrite; good for production. Optional: add a COMMENT for future maintainers.

     await queryInterface.addColumn(
       tableName,
-      column,
+      secondaryMappingsColumn,
       { type: DataTypes.JSONB, allowNull: true },
       { transaction }
     );
+    await queryInterface.sequelize.query(
+      "COMMENT ON COLUMN patient_mapping.secondary_mappings IS " +
+        "'Secondary EHR-specific mappings used by write-back flows (e.g., Athena).'",
+      { transaction }
+    );

18-22: Optional guard in down() to be resilient if partially applied.

Not required, but avoids failures if the column was already removed in a partial rollback.

-export const down: Migration = ({ context: queryInterface }) => {
-  return queryInterface.sequelize.transaction(async transaction => {
-    await queryInterface.removeColumn(tableName, column, { transaction });
-  });
-};
+export const down: Migration = ({ context: queryInterface }) => {
+  return queryInterface.sequelize.transaction(async transaction => {
+    const table = await queryInterface.describeTable(tableName);
+    if (table.secondary_mappings) {
+      await queryInterface.removeColumn(tableName, secondaryMappingsColumn, { transaction });
+    }
+  });
+};

4-16: Future-readiness: index only if you’ll query inside JSON.

If lookups will filter by specific JSON paths (e.g., departmentId inside secondary_mappings), consider a GIN index or a functional index later. Skip for now if reads are by PK and the JSON is only fetched.

📜 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 f84b52e and 6d7e224.

📒 Files selected for processing (1)
  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-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/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

Use types whenever possible

Files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.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/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
🧠 Learnings (6)
📚 Learning: 2025-04-26T22:17:45.248Z
Learnt from: leite08
PR: metriport/metriport#3412
File: packages/api/src/models/medical/patient-import-mapping.ts:16-41
Timestamp: 2025-04-26T22:17:45.248Z
Learning: The patient_import_mapping table already has a unique constraint on the fields "job_id", "row_number", and "data_pipeline_request_id" defined in the migration file 2025-04-24_02_create-patient_import_mapping-table.ts, which ensures data integrity.

Applied to files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
📚 Learning: 2025-06-18T08:03:50.674Z
Learnt from: RamilGaripov
PR: metriport/metriport#3962
File: packages/api/src/models/medical/patient-cohort.ts:15-21
Timestamp: 2025-06-18T08:03:50.674Z
Learning: In the Metriport codebase, Sequelize models typically don't define `allowNull: false` or `references` metadata for foreign key columns. These constraints are handled exclusively in migration files, keeping models simpler and avoiding duplication. This is a consistent pattern across recent models like CohortModel, CoverageEnhancementModel, DocRefMappingModel, etc. The migration files properly define all FK constraints, NOT NULL requirements, and cascade behaviors.

Applied to files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.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/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
📚 Learning: 2025-04-28T22:58:19.669Z
Learnt from: leite08
PR: metriport/metriport#3749
File: packages/api/src/models/medical/patient-import-mapping.ts:16-41
Timestamp: 2025-04-28T22:58:19.669Z
Learning: For Sequelize models in this codebase, field constraints (such as `allowNull: false`) and field mappings (such as `field: "column_name"`) are defined in the migrations, not in the model definitions themselves.

Applied to files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
📚 Learning: 2025-04-07T17:32:22.935Z
Learnt from: leite08
PR: metriport/metriport#3624
File: packages/api/src/command/medical/patient/get-patient.ts:90-97
Timestamp: 2025-04-07T17:32:22.935Z
Learning: When dealing with raw SQL queries in Sequelize that return complex data structures (especially with joins and JSON aggregation), using `any` type assertions may be necessary since TypeScript cannot infer the types. Using constants for column aliases helps minimize maintenance issues in these cases.

Applied to files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
📚 Learning: 2025-06-18T08:03:50.674Z
Learnt from: RamilGaripov
PR: metriport/metriport#3962
File: packages/api/src/models/medical/patient-cohort.ts:15-21
Timestamp: 2025-06-18T08:03:50.674Z
Learning: In the Metriport codebase, Sequelize models typically don't define `allowNull: false` or `references` metadata for foreign key columns. These constraints are handled exclusively in migration files, keeping models simpler and avoiding duplication. This is a consistent pattern across recent models like CohortModel, CoverageEnhancementModel, DocRefMappingModel, etc.

Applied to files:

  • packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts
⏰ 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). (3)
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
packages/api/src/sequelize/migrations/2025-08-29_00_add-secondary-mappings-to-patient-mapping.ts (1)

1-2: Imports and typing look correct.

Consistent with prior migrations; follows the pattern noted in the learnings (constraints/metadata in migrations).

metriport/metriport-internal#1040

Ref:
Signed-off-by: Alexey Sergeev <alexey@metriport.com>
Thomas Yopes added 3 commits September 4, 2025 13:52
Ref: ENG-585

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

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

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

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

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
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

♻️ Duplicate comments (3)
packages/core/src/external/ehr/athenahealth/index.ts (1)

1681-1693: By-ID short-circuit now forwards resourceId — good fix.

Prevents cache-key collisions for by-id lookups. Consider Set-based predicate here too for parity.

-    if (resourceType === "MedicationStatement") {
+    if (writeBackOnlyResources.has(resourceType as ResourceType)) {
       return await fetchEhrBundleUsingCache({
         ehr: EhrSources.athena,
         cxId,
         metriportPatientId,
         ehrPatientId: athenaPatientId,
         resourceType,
         resourceId,
         fetchResourcesFromEhr: () => Promise.resolve([]),
         useCachedBundle,
       });
     }
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (2)

149-151: Keep sort logic as-is for parity with internal repo.

Per prior guidance, do not change this comparator.


118-133: Vitals parsing bug: decimals dropped; zero treated as missing.

parseInt truncates; falsy check excludes 0. This corrupts values and skips valid observations.

Apply:

-    const observationValue = v.valueQuantity?.value ?? v.valueString;
+    const observationValue = v.valueQuantity?.value ?? v.valueString;
     const date = v.effectiveDateTime ?? v.effectivePeriod?.start;
-    if (!date || !observationValue) {
+    if (!date || observationValue == null) {
       results.push({ title, mostRecentObservation: v });
       return;
     }
-
-    const observationPoint: GroupedObservation = {
-      rawVital: v,
-      date,
-      observation:
-        typeof observationValue === "number" ? observationValue : parseInt(observationValue),
-      // TODO: Make sure all data points have the same unit
-      unit: v.valueQuantity?.unit?.replace(/[{()}]/g, ""),
-    };
+    const parsedValue =
+      typeof observationValue === "number"
+        ? observationValue
+        : parseFloat(String(observationValue));
+    if (!Number.isFinite(parsedValue)) {
+      results.push({ title, mostRecentObservation: v });
+      return;
+    }
+    const observationPoint: GroupedObservation = {
+      rawVital: v,
+      date,
+      observation: parsedValue,
+      // TODO: Make sure all data points have the same unit
+      unit: v.valueQuantity?.unit?.replace(/[{()}]/g, ""),
+    };
🧹 Nitpick comments (11)
packages/core/src/external/ehr/athenahealth/index.ts (4)

232-239: Centralize write-back–only resources to avoid stringly-typed checks.

Define a Set and reuse it across scopes and short-circuits, instead of hardcoding "MedicationStatement" in multiple places.

Apply the Set definition near resource lists:

 export const supportedAthenaHealthResources: ResourceType[] = [
   "AllergyIntolerance",
   "Condition",
   "DiagnosticReport",
   "Immunization",
   "MedicationRequest",
-  "MedicationStatement", // NOT REALLY SUPPORTED
+  "MedicationStatement", // write-back only (no read support)
   "Observation",
   "Procedure",
   "Encounter",
 ];
+
+// Resources we allow for write-back flows but do not fetch from Athena
+const writeBackOnlyResources = new Set<ResourceType>(["MedicationStatement"]);

254-258: Derive OAuth scopes from the Set (avoid drift).

Use the writeBackOnlyResources Set so the filtering logic stays consistent with future additions.

-export const scopes = [
-  ...supportedAthenaHealthResources,
-  ...supportedAthenaHealthReferenceResources,
-].filter(resource => resource !== "MedicationStatement");
+export const scopes = [
+  ...supportedAthenaHealthResources,
+  ...supportedAthenaHealthReferenceResources,
+].filter(resource => !writeBackOnlyResources.has(resource));

436-476: Include query params in additionalInfo for better observability.

Helps debug issues (e.g., department context).

-    const additionalInfo = { cxId, practiceId: this.practiceId, patientId };
+    const additionalInfo = {
+      cxId,
+      practiceId: this.practiceId,
+      patientId,
+      queryParams: queryParams.toString(),
+    };

1558-1569: Short-circuit for write-back–only types is correct; reuse the Set.

This returns a cached empty bundle as intended; switch to Set-based check for consistency.

-    if (resourceType === "MedicationStatement") {
+    if (writeBackOnlyResources.has(resourceType as ResourceType)) {
       return await fetchEhrBundleUsingCache({
         ehr: EhrSources.athena,
         cxId,
         metriportPatientId,
         ehrPatientId: athenaPatientId,
         resourceType,
         fetchResourcesFromEhr: () => Promise.resolve([]),
         useCachedBundle,
       });
     }
packages/core/src/external/ehr/command/write-back/shared.ts (2)

95-117: Lab-panel: empty secondary array passes validation. Confirm intent.

Array.every returns true for [], so an empty secondaries list will pass and write back with 0 observations.

If you want to require at least one Observation:

-    if (
-      !params.secondaryResourceOrResources ||
-      !params.secondaryResourceOrResources.every(isObservation)
-    ) {
+    if (
+      !Array.isArray(params.secondaryResourceOrResources) ||
+      params.secondaryResourceOrResources.length < 1 ||
+      !params.secondaryResourceOrResources.every(isObservation)
+    ) {

123-162: Medication-statement: unify error text and single guard for “no medications”.

Minor DX: duplicate error and a follow-up empty check.

Apply:

-    if (
-      !params.secondaryResourceOrResources ||
-      !params.secondaryResourceOrResources.every(isMedication)
-    ) {
-      throw new BadRequestError(
-        "MedicationStatement write back requires secondary resources",
-        undefined,
-        {
-          ehr: params.ehr,
-          writeBackResource: params.writeBackResource,
-        }
-      );
-    }
-    const medication = params.secondaryResourceOrResources[0];
-    if (!medication) {
+    if (
+      !Array.isArray(params.secondaryResourceOrResources) ||
+      params.secondaryResourceOrResources.length < 1 ||
+      !params.secondaryResourceOrResources.every(isMedication)
+    ) {
       throw new BadRequestError(
-        "MedicationStatement write back requires secondary resources",
+        "MedicationStatement write back requires at least one Medication in secondary resources",
         undefined,
         {
           ehr: params.ehr,
           writeBackResource: params.writeBackResource,
         }
       );
     }
+    const medication = params.secondaryResourceOrResources[0];
packages/core/src/external/ehr/command/write-back/grouped-vitals.ts (1)

105-116: Iteration nit: use forEach instead of map when not using return value.

Slight readability win, no behavior change.

-  observations.map(v => {
+  observations.forEach(v => {
     ...
-  });
+  });

Also applies to: 143-144

packages/core/src/external/ehr/job/bundle/write-back-bundles/ehr-write-back-resource-diff-bundles-direct.ts (1)

304-330: Hydration is O(M×S); index statements by medication for O(M+S).

Improves scalability without changing behavior.

Apply:

-async function hydrateMedications({
-  medications,
-  statements,
-}: {
-  medications: Medication[];
-  statements: MedicationStatement[];
-}): Promise<{ medication: Medication; statements: MedicationStatement[] }[]> {
-  const hydratedMetriportOnlyResources: {
-    medication: Medication;
-    statements: MedicationStatement[];
-  }[] = [];
-  for (const medication of medications) {
-    if (!medication.id) {
-      hydratedMetriportOnlyResources.push({ medication, statements: [] });
-      continue;
-    }
-    const hydratedStatements: MedicationStatement[] = [];
-    for (const statement of statements) {
-      if (!statement.medicationReference || !statement.medicationReference.reference) continue;
-      const ref = statement.medicationReference.reference;
-      if (ref !== `Medication/${medication.id}`) continue;
-      hydratedStatements.push(statement);
-    }
-    hydratedMetriportOnlyResources.push({ medication, statements: hydratedStatements });
-  }
-  return hydratedMetriportOnlyResources;
-}
+async function hydrateMedications({
+  medications,
+  statements,
+}: {
+  medications: Medication[];
+  statements: MedicationStatement[];
+}): Promise<{ medication: Medication; statements: MedicationStatement[] }[]> {
+  const byMedicationId = new Map<string, MedicationStatement[]>();
+  for (const s of statements) {
+    const ref = s.medicationReference?.reference; // "Medication/{id}"
+    if (!ref?.startsWith("Medication/")) continue;
+    const id = ref.slice("Medication/".length);
+    if (!id) continue;
+    const arr = byMedicationId.get(id) ?? [];
+    arr.push(s);
+    byMedicationId.set(id, arr);
+  }
+  return medications.map(m => ({
+    medication: m,
+    statements: m.id ? byMedicationId.get(m.id) ?? [] : [],
+  }));
+}
packages/api/src/external/ehr/athenahealth/command/update-patient-mapping-department-id.ts (3)

5-10: Param type is clear; consider immutability.
Optional: mark fields as readonly to signal intent and help TS catch accidental mutation.

-export type UpdateAthenaPatientMappingDepartmentIdParams = {
-  cxId: string;
-  athenaPatientId: string;
-  athenaDepartmentId: string;
-};
+export type UpdateAthenaPatientMappingDepartmentIdParams = {
+  readonly cxId: string;
+  readonly athenaPatientId: string;
+  readonly athenaDepartmentId: string;
+};

11-15: Doc should clarify persistence vs. context-bound department usage.
Per retrieved learnings, departmentId is context-sensitive in Athena (patient can be opened from multiple departments). Please state in the docstring whether persisting departmentId in secondaryMappings is the intended long-term model, or if this is a transitional mechanism for write-back only.


21-26: Tighten typing by specializing the generic to Athena’s mappings.
Without binding T, parsedSecondaryMappings may be inferred as a broad union, weakening type-safety around departmentId. Import the Athena-specific type and specialize the call.

-import { EhrSources } from "@metriport/shared/interface/external/ehr/source";
+import { EhrSources } from "@metriport/shared/interface/external/ehr/source";
+import { AthenaPatientMappingSecondaryMappings } from "../shared";
@@
-  const { parsedSecondaryMappings, patientMapping } =
-    await getPatientMappingAndParsedSecondaryMappings({
+  const { parsedSecondaryMappings, patientMapping } =
+    await getPatientMappingAndParsedSecondaryMappings<AthenaPatientMappingSecondaryMappings>({
       ehr: EhrSources.athena,
       cxId,
       ehrPatientId: athenaPatientId,
     });

Thomas Yopes added 2 commits September 5, 2025 13:22
Ref: ENG-585

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

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

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Copy link
Contributor

@keshavsaharia keshavsaharia left a comment

Choose a reason for hiding this comment

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

If you could just fix that one tiny typo (add an "e" to Statment), everything else looks good to go! 🚀

Thomas Yopes added 5 commits September 5, 2025 13:33
Ref: ENG-585

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

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

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

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

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Mac.attlocal.net>
Copy link
Contributor

@keshavsaharia keshavsaharia left a comment

Choose a reason for hiding this comment

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

Approving again to note that I have reviewed all the changes up to this point.

@thomasyopes thomasyopes added this pull request to the merge queue Sep 5, 2025
Merged via the queue into develop with commit 9e80722 Sep 5, 2025
17 checks passed
@thomasyopes thomasyopes deleted the eng-585-athena-auto-writeback branch September 5, 2025 21:03
@thomasyopes thomasyopes 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.

3 participants