8000 feat(healthie): integration v0 by thomasyopes · Pull Request #3754 · metriport/metriport · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(healthie): integration v0 #3754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
May 1, 2025
Merged

feat(healthie): integration v0 #3754

merged 35 commits into from
May 1, 2025

Conversation

thomasyopes
Copy link
Contributor
@thomasyopes thomasyopes commented Apr 29, 2025

Ref: ENG-82

Ref: #1040

Issues:

Dependencies

Description

  • initial healthie integraiton including
  • Routing
  • Commands
  • Client

Testing

  • Local
    • creating cx-mapping with WHs works
    • ehr-dash linking works
    • ehr-dash MAPI works
    • ehr-dash sync-patient works
    • appointments endpoint works
    • appointments 48hrs endpoint works
    • appointment created WH works
    • patient created WH works
  • Staging
    • creating cx-mapping with WHs works
    • ehr-dash linking works
    • ehr-dash MAPI works
    • ehr-dash sync-patient works
    • appointments endpoint works
    • appointments 48hrs endpoint works
    • linking queue + endpoint works
    • appointment created WH works
    • patient created WH works
  • Sandbox
    • N/A
  • Production
    • creating cx-mapping with WHs works
    • ehr-dash linking works
    • ehr-dash MAPI works
    • ehr-dash sync-patient works
    • appointments endpoint works
    • appointments 48hrs endpoint works
    • linking queue + endpoint works
    • appointment created WH works
    • patient created WH works

Release Plan

  • Upstream dependencies are met/released
  • Merge this

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced comprehensive integration with the Healthie EHR platform, including patient and appointment synchronization, webhook subscription and handling, and secure linking between Healthie and Metriport.
    • Added new API endpoints and Express routers for Healthie patient and appointment processing, both externally and internally.
    • Implemented background processing for Healthie appointments and patient linking using AWS SQS queues and Lambda functions.
    • Enabled robust webhook signature verification and JWT token management for Healthie authentication.
    • Provided utilities for creating and managing Healthie API clients, mappings, and environment configurations.
  • Documentation

    • Updated API documentation to include new query parameters for AthenaHealth, Elation, and Healthie endpoints, reflecting enhanced integration capabilities.
  • Chores

    • Extended infrastructure and configuration to support Healthie integration, including new queues, Lambdas, secret management, and environment variables.
  • Bug Fixes

    • Standardized HTTP header capitalization in AthenaHealth and Elation integrations.
    • Improved content-type header detection to be case-insensitive in HTTP requests.
    • Enhanced error handling in appointment fetching to silently handle known errors without logging.

Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Copy link
linear bot commented Apr 29, 2025

Copy link
coderabbitai bot commented Apr 29, 2025

Walkthrough

This set of changes introduces comprehensive support for the "Healthie" EHR integration throughout the codebase. It adds new infrastructure components (SQS queue, Lambda), API client, data schemas, and routes for Healthie, mirroring the architecture used for existing EHR integrations. The backend now supports Healthie patient synchronization, appointment processing, webhook subscription and handling, JWT token management, and internal admin endpoints. Type definitions, validation schemas, and configuration utilities are added for Healthie-specific data and flows. The API, core logic, and infrastructure are updated to orchestrate Healthie-related operations, including background processing and secure webhook verification.

Changes

File(s) / Path(s) Change Summary
packages/shared/src/interface/external/ehr/source.ts Added healthie to EhrSources enum and related arrays/types.
packages/shared/src/interface/external/ehr/healthie/ (all files) Introduced new modules for Healthie: patient, appointment, event, subscription, jwt-token, cx-mapping schemas/types, and an index aggregator.
packages/shared/src/domain/secrets.ts Added cxApiKeyMapSecretSchema for Healthie API key mapping.
packages/api/src/shared/config.ts Added static methods to Config for Healthie environment and API key map retrieval.
packages/api/src/external/ehr/shared.ts Extended shared types/constants to support Healthie; added a utility for look-forward time ranges with offset.
packages/api/src/external/ehr/healthie/ (all files) Added modules for Healthie: shared utilities, patient sync, appointment processing, webhook subscription, and appointment-to-patient lookup.
packages/api/src/external/ehr/elation/command/sync-patient.ts Added dayjs duration plugin and token duration constants.
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts Added specific handling to suppress logging and error return for BadRequestError and NotFoundError in appointment fetching.
packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts Added specific handling to suppress logging and error return for BadRequestError and NotFoundError in appointment fetching.
packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts Added specific handling to suppress logging and error return for BadRequestError and NotFoundError in appointment fetching.
packages/core/src/external/ehr/healthie/index.ts Implemented Healthie API client with methods for patient, appointment, subscription management, and webhook subscription.
packages/core/src/external/ehr/healthie/link-patient/ (all files) Added Healthie link patient handler interface and two implementations: cloud (SQS-based) and local (direct API call), plus a factory function for instantiation based on environment.
packages/core/src/external/ehr/api/link-patient.ts Generalized linkPatient API to accept EHR source; updated types and error handling.
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts Updated to use generalized linkPatient with explicit EHR source.
packages/core/src/external/ehr/shared.ts Made content-type header checks case-insensitive; improved URL handling in requests; added retry logic excluding 400/404 errors.
packages/core/src/util/config.ts Added static method for Healthie link patient queue URL retrieval.
packages/core/src/util/webhook.ts Added HMAC-SHA256 signature verification for Healthie webhooks, including canonical string construction and signature generation.
packages/core/src/external/ehr/athenahealth/index.ts, packages/core/src/external/ehr/elation/index.ts Standardized HTTP header casing to "Content-Type".
packages/api/src/routes/ehr/healthie/ (all files) Added routers for Healthie patient endpoints, webhook handlers, route aggregators, and authentication middleware.
packages/api/src/routes/ehr/athenahealth/patient.ts, .../elation/patient.ts, .../internal/ehr/athenahealth/patient.ts, .../internal/ehr/elation/patient.ts Updated JSDoc comments to accurately reflect query parameters for patient endpoints.
packages/api/src/routes/ehr/elation/auth/middleware.ts Moved import of webhook signature verifier to top of file.
packages/api/src/routes/internal/ehr/healthie/ (all files) Added internal routers for Healthie patient operations, secret key retrieval, and route aggregation.
packages/api/src/routes/internal/index.ts Added conditional support for Healthie webhook subscription in customer mapping route.
packages/api/src/routes/internal/jwt-token/healthie.ts Added internal routes for Healthie JWT token check and save operations.
packages/api/src/routes/internal/jwt-token/index.ts Added Healthie route to JWT token router.
packages/api/src/routes/internal/ehr/index.ts Registered Healthie internal EHR router.
packages/api/src/routes/ehr/index.ts Registered Healthie dashboard and webhook routes with corresponding middleware.
packages/infra/config/env-config.ts Added Healthie configuration block to environment config type.
packages/infra/lib/api-stack.ts Added Healthie link patient queue and lambda to API stack; included in Fargate service and environment variables.
packages/infra/lib/api-stack/api-service.ts Added Healthie link patient queue to service creation and environment, with permission grants.
packages/infra/lib/ehr-nested-stack.ts Added Healthie link patient queue and lambda setup, with configuration and resource creation; tuned memory and alarm settings for some Lambdas.
packages/infra/lib/secrets-stack.ts Included Healthie secrets in EHR secrets aggregation.
packages/shared/src/interface/external/ehr/elation/index.ts Re-exported additional modules for completeness.
packages/shared/src/interface/external/ehr/healthie/index.ts Added new module aggregating Healthie-specific exports.
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts Added Healthie JWT token data schema and type.
packages/shared/src/interface/external/ehr/healthie/patient.ts Added Healthie patient data schemas and types, including GraphQL response shapes and quick notes update.
packages/shared/src/interface/external/ehr/healthie/appointment.ts Added Healthie appointment data schemas and types with GraphQL response validation.
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts Added Healthie secondary mappings schema with webhook and processing flags.
packages/shared/src/interface/external/ehr/healthie/event.ts Added Healthie event schemas and types for appointment and patient creation events.
packages/shared/src/interface/external/ehr/healthie/subscription.ts Added Healthie subscription schemas, types, and GraphQL response validation.
packages/shared/src/common/retry.ts Reduced default documented maxAttempts for retry logic from 10 to 5.
packages/core/src/util/sqs.ts Reduced max SQS message size and increased batch size constants.

Sequence Diagram(s)

Healthie Patient Sync Flow

sequenceDiagram
    participant Client
    participant API
    participant HealthieApi
    participant MetriportDB

    Client->>API: POST /ehr/healthie/patient/:id (with cxId, practiceId)
    API->>HealthieApi: getPatient({cxId, patientId})
    HealthieApi-->>API: Patient data
    API->>MetriportDB: Find or create Metriport patient
    MetriportDB-->>API: Metriport patient ID
    API->>HealthieApi: updatePatientQuickNotesWithLink({cxId, patientId, link})
    API-->>Client: { patientId: ... }
Loading

Healthie Appointment Webhook Flow

sequenceDiagram
    participant HealthieWebhook
    participant API
    participant HealthieApi
    participant LinkPatientHandler

    HealthieWebhook->>API: POST /ehr/webhook/healthie/:practiceId/appointment/created
    API->>HealthieApi: getAppointment({cxId, appointmentId})
    HealthieApi-->>API: Appointment with attendee
    API->>HealthieApi: updatePatientQuickNotesWithLink({cxId, patientId, link})
    API->>LinkPatientHandler: processLinkPatient({cxId, practiceId, patientId})
    API-->>HealthieWebhook: 200 OK
Loading

Healthie Webhook Subscription Flow

sequenceDiagram
    participant Admin
    participant API
    participant HealthieApi
    participant MetriportDB

    Admin->>API: POST /internal/cx-mapping (with source=healthie)
    API->>HealthieApi: subscribeToResource({cxId, practiceId, resource})
    HealthieApi-->>API: Subscription details (event type, url, secret)
    API->>MetriportDB: Update cx-mapping secondaryMappings with webhook info
    API-->>Admin: 200 OK
Loading

Healthie Background Appointment Processing

sequenceDiagram
    participant Scheduler
    participant API
    participant HealthieApi
    participant LinkPatientHandler

    Scheduler->>API: Trigger processPatientsFromAppointments
    API->>HealthieApi: getAppointments({cxId, practiceId, date range})
    HealthieApi-->>API: List of appointments
    loop For each unique patient
        API->>LinkPatientHandler: processLinkPatient({cxId, practiceId, patientId})
        API->>API: syncHealthiePatientIntoMetriport(...)
    end
    API-->>Scheduler: Done
Loading

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Thomas Yopes added 14 commits April 29, 2025 11:46
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes marked this pull request as ready for review April 30, 2025 20:17
@thomasyopes thomasyopes changed the title feat(healthie): init commit feat(healthie): integration v0 Apr 30, 2025
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: 17

🧹 Nitpick comments (27)
packages/api/src/routes/ehr/healthie/routes/webhook.ts (1)

7-8: Consider adding input validation middleware

While the implementation is clean, consider adding input validation middleware for the :practiceId parameter to ensure it's in the expected format before reaching the handlers.

 const routes = Router();
 
+// Middleware to validate practiceId
+routes.param('practiceId', (req, res, next, practiceId) => {
+  // Example: Check if practiceId is a valid UUID or meets specific format requirements
+  if (!/^[a-zA-Z0-9-_]+$/.test(practiceId)) {
+    return res.status(400).json({ error: 'Invalid practiceId format' });
+  }
+  next();
+});
+
 routes.use("/:practiceId/appointment", appointmentWebhook);
 routes.use("/:practiceId/patient", patientWebhook);
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (1)

6-13: Function looks good, but consider error handling for the queue URL retrieval.

The factory function properly implements environment-specific handlers, following the factory pattern well. In non-development environments, Config.getHealthieLinkPatientQueueUrl() might throw if the environment variable is missing, which would be uncaught in the current implementation.

Consider adding error handling for the case where the queue URL cannot be retrieved in non-development environm 8000 ents:

  if (Config.isDev()) {
    const waitTimeAtTheEndInMillis = 0;
    return new HealthieLinkPatientLocal(waitTimeAtTheEndInMillis);
  }
-  const healthieLinkPatientQueueUrl = Config.getHealthieLinkPatientQueueUrl();
-  return new HealthieLinkPatientCloud(healthieLinkPatientQueueUrl);
+  try {
+    const healthieLinkPatientQueueUrl = Config.getHealthieLinkPatientQueueUrl();
+    return new HealthieLinkPatientCloud(healthieLinkPatientQueueUrl);
+  } catch (error) {
+    throw new Error(`Failed to create Healthie link patient handler: ${error.message}`);
+  }
packages/api/src/routes/internal/ehr/healthie/secret-key.ts (1)

26-28: Consider adding more specific error message.

The error handling for invalid resources is good, but consider making the error message more specific to guide API consumers.

  if (!isSubscriptionResource(resource)) {
-   throw new BadRequestError("Invalid resource", undefined, { resource });
+   throw new BadRequestError(
+     `Invalid subscription resource: '${resource}'`,
+     undefined,
+     { resource, validResources: "<list of valid resources>" }
+   );
  }
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)

6-22: Solid implementation with effective parameter handling.

The class correctly implements the HealthieLinkPatientHandler interface and properly uses the linkPatient function with appropriate parameters. The conditional wait time is a good feature for testing/development.

Consider adding logging to help with debugging and monitoring, especially around the linkPatient call:

  async processLinkPatient({
    cxId,
    practiceId,
    patientId,
  }: ProcessLinkPatientRequest): Promise<void> {
+   const { log } = out(`Healthie processLinkPatient - cxId ${cxId}`);
+   log(`Processing link patient request for practiceId: ${practiceId}, patientId: ${patientId}`);
    await linkPatient({
      ehr: EhrSources.healthie,
      cxId,
      practiceId,
      patientId,
    });
+   log(`Successfully processed link patient request`);
    if (this.waitTimeInMillis > 0) await sleep(this.waitTimeInMillis);
  }
packages/api/src/routes/ehr/healthie/patient.ts (2)

13-37: Refactor to eliminate duplication between GET and POST handlers.

The GET and POST routes have identical implementation which creates unnecessary code duplication. While supporting both HTTP methods might be intentional, the duplicated code should be extracted into a shared function.

// Add a shared handler function
+const handlePatientSync = async (req: Request, res: Response): Promise<Response> => {
+  const cxId = getCxIdOrFail(req);
+  const healthiePatientId = getFrom("params").orFail("id", req);
+  const healthiePracticeId = getFromQueryOrFail("practiceId", req);
+  const patientId = await syncHealthiePatientIntoMetriport({
+    cxId,
+    healthiePracticeId,
+    healthiePatientId,
+  });
+  return res.status(httpStatus.OK).json(patientId);
+};

// Use the shared handler in both routes
-router.get(
-  "/:id",
-  handleParams,
-  processEhrPatientId(tokenEhrPatientIdQueryParam, "params"),
-  requestLogger,
-  asyncHandler(async (req: Request, res: Response) => {
-    const cxId = getCxIdOrFail(req);
-    const healthiePatientId = getFrom("params").orFail("id", req);
-    const healthiePracticeId = getFromQueryOrFail("practiceId", req);
-    const patientId = await syncHealthiePatientIntoMetriport({
-      cxId,
-      healthiePracticeId,
-      healthiePatientId,
-    });
-    return res.status(httpStatus.OK).json(patientId);
-  })
-);
+router.get(
+  "/:id",
+  handleParams,
+  processEhrPatientId(tokenEhrPatientIdQueryParam, "params"),
+  requestLogger,
+  asyncHandler(handlePatientSync)
+);

-router.post(
-  "/:id",
-  handleParams,
-  processEhrPatientId(tokenEhrPatientIdQueryParam, "params"),
-  requestLogger,
-  asyncHandler(async (req: Request, res: Response) => {
-    const cxId = getCxIdOrFail(req);
-    const healthiePatientId = getFrom("params").orFail("id", req);
-    const healthiePracticeId = getFromQueryOrFail("practiceId", req);
-    const patientId = await syncHealthiePatientIntoMetriport({
-      cxId,
-      healthiePracticeId,
-      healthiePatientId,
-    });
-    return res.status(httpStatus.OK).json(patientId);
-  })
-);
+router.post(
+  "/:id",
+  handleParams,
+  processEhrPatientId(tokenEhrPatientIdQueryParam, "params"),
+  requestLogger,
+  asyncHandler(handlePatientSync)
+);

Also applies to: 39-63


21-37: Consider adding informative logging.

The current implementation has no custom logging in the route handlers. Adding logging would help trace API requests and monitor synchronization operations.

asyncHandler(async (req: Request, res: Response) => {
  const cxId = getCxIdOrFail(req);
  const healthiePatientId = getFrom("params").orFail("id", req);
  const healthiePracticeId = getFromQueryOrFail("practiceId", req);
+ console.log(`Syncing Healthie patient ${healthiePatientId} from practice ${healthiePracticeId} for customer ${cxId}`);
  const patientId = await syncHealthiePatientIntoMetriport({
    cxId,
    healthiePracticeId,
    healthiePatientId,
  });
+ console.log(`Successfully synced Healthie patient ${healthiePatientId} to Metriport patient ${patientId}`);
  return res.status(httpStatus.OK).json(patientId);
})

Also applies to: 47-63

packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)

1-26: Add logging for traceability

Consider adding logging to capture the start and completion of the operation for better traceability.

+import { out } from "../../../../util/log";
 import { createUuidFromText } from "@metriport/shared/common/uuid";
 import { Config } from "../../../../util/config";
 import { SQSClient } from "../../../aws/sqs";
 import { HealthieLinkPatientHandler, ProcessLinkPatientRequest } from "./healthie-link-patient";

 export class HealthieLinkPatientCloud implements HealthieLinkPatientHandler {
   private readonly sqsClient: SQSClient;

   constructor(
     private readonly healthieLinkPatientQueueUrl: string,
     region?: string,
     sqsClient?: SQSClient
   ) {
     this.sqsClient = sqsClient ?? new SQSClient({ region: region ?? Config.getAWSRegion() });
   }

   async processLinkPatient(params: ProcessLinkPatientRequest): Promise<void> {
+    const { log } = out(`Healthie.LinkPatient.Cloud - cxId ${params.cxId}`);
     const { cxId } = params;
     const payload = JSON.stringify(params);
+    log(`Enqueueing link patient request`);
     await this.sqsClient.sendMessageToQueue(this.healthieLinkPatientQueueUrl, payload, {
       fifo: true,
       messageDeduplicationId: createUuidFromText(payload),
       messageGroupId: cxId,
     });
+    log(`Successfully enqueued link patient request`);
   }
 }
packages/core/src/external/ehr/api/link-patient.ts (1)

31-35: Consider adding validation for the API response

While the function checks if response.data exists, it might be worth adding more specific validation of the response structure based on the expected format from different EHR sources.

  try {
    const response = await api.post(linkPatientUrl);
-   if (!response.data) throw new Error(`No body returned from ${linkPatientUrl}`);
+   if (!response.data) {
+     throw new Error(`No body returned from ${linkPatientUrl} for ehr: ${ehr}`);
+   }
+   if (response.status !== 200) {
+     throw new Error(`Unexpected status code: ${response.status} from ${linkPatientUrl} for ehr: ${ehr}`);
+   }
    debug(`${linkPatientUrl} resp: ${JSON.stringify(response.data)}`);
  } catch (error) {
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)

1-10: Consider adding additional token data validation

JWT tokens often include standard claims like exp (expiration time), iat (issued at time), or sub (subject). Consider extending the schema to include these standard JWT claims for more comprehensive validation.

 import z from "zod";
 import { EhrSources } from "../source";

 export const healthieDashSource = EhrSources.healthie as const;
 export const healthieDashJwtTokenDataSchema = z.object({
   practiceId: z.string(),
   patientId: z.string(),
   source: z.literal(`${healthieDashSource}`),
+  exp: z.number().optional(), // Expiration time
+  iat: z.number().optional(), // Issued at time
+  sub: z.string().optional(), // Subject (typically user ID)
 });
 export type HealthieDashJwtTokenData = z.infer<typeof healthieDashJwtTokenDataSchema>;
packages/api/src/routes/internal/jwt-token/healthie.ts (1)

1-54: Healthie JWT token routes look good with minor suggestions.

The implementation provides clean endpoints for checking and saving JWT tokens specifically for Healthie integration.

I suggest a couple of improvements:

  1. Consider extracting the createJwtSchema to a shared location if it's used in multiple places in the codebase
  2. Adding some basic validation for the JWT token format before passing it to the token operations
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)

9-19: Consider adding JSDoc comments for feature flags.

The secondary mappings schema includes multiple boolean flags controlling different features. While the names are descriptive, adding JSDoc comments would improve maintainability by explaining the purpose and implications of each flag.

For example:

+ /**
+  * When enabled, disables patient linking when receiving patient webhooks
+  */
  webhookPatientPatientLinkingDisabled: z.boolean().optional(),

9-19: Consider grouping related flags into sub-objects.

There are several related boolean flags that could be organized into logical groupings to improve readability and maintainability:

- webhookPatientPatientLinkingDisabled: z.boolean().optional(),
- webhookPatientPatientProcessingEnabled: z.boolean().optional(),
- webhookAppointmentPatientLinkingDisabled: z.boolean().optional(),
- webhookAppointmentPatientProcessingDisabled: z.boolean().optional(),
+ webhookPatient: z.object({
+   patientLinkingDisabled: z.boolean().optional(),
+   patientProcessingEnabled: z.boolean().optional(),
+ }).optional(),
+ webhookAppointment: z.object({
+   patientLinkingDisabled: z.boolean().optional(),
+   patientProcessingDisabled: z.boolean().optional(),
+ }).optional(),

This would make the configuration more hierarchical and self-documenting.

packages/api/src/routes/internal/ehr/healthie/patient.ts (4)

25-27: Fire-and-forget pattern should add more context to error messages.

The code uses the fire-and-forget pattern (not awaiting the promise) with error handling via .catch(). This is acceptable, but consider enhancing the error context:

processPatientsFromAppointments({ lookupMode: LookupModes.Appointments }).catch(
-  processAsyncError("Healthie processPatientsFromAppointments")
+  processAsyncError(`Healthie processPatientsFromAppointments - lookupMode: ${LookupModes.Appointments}`)
);

This would make debugging easier by providing more specific context in error logs.


60-74: Consider using POST body instead of query parameters.

This endpoint uses query parameters for data that modifies resources. Following RESTful principles, consider using the request body for this data:

asyncHandler(async (req: Request, res: Response) => {
-  const cxId = getUUIDFrom("query", req, "cxId").orFail();
-  const healthiePatientId = getFromQueryOrFail("patientId", req);
-  const healthiePracticeId = getFromQueryOrFail("practiceId", req);
-  const triggerDq = getFromQueryAsBoolean("triggerDq", req);
+  const { cxId, patientId: healthiePatientId, practiceId: healthiePracticeId, triggerDq = false } = req.body;
+  
+  // Validate required fields
+  if (!cxId || !healthiePatientId || !healthiePracticeId) {
+    throw new BadRequestError("Missing required fields: cxId, patientId, practiceId");
+  }
  
  syncHealthiePatientIntoMetriport({
    cxId,
    healthiePracticeId,
    healthiePatientId,
    triggerDq,
  }).catch(processAsyncError("Healthie syncHealthiePatientIntoMetriport"));
  return res.sendStatus(httpStatus.OK);
})

Additionally, using a request body schema with zod would provide better validation.


67-73: Add more context to the error logs for better debugging.

Enhance the error context with patient and practice information:

syncHealthiePatientIntoMetriport({
  cxId,
  healthiePracticeId,
  healthiePatientId,
  triggerDq,
}).catch(
-  processAsyncError("Healthie syncHealthiePatientIntoMetriport")
+  processAsyncError(`Healthie syncHealthiePatientIntoMetriport for cxId: ${cxId}, patientId: ${healthiePatientId}`)
);

87-99: Apply consistent pattern for request data handling.

For consistency with the previous suggestion, consider using request body instead of query parameters here as well:

asyncHandler(async (req: Request, res: Response) => {
-  const cxId = getUUIDFrom("query", req, "cxId").orFail();
-  const healthiePatientId = getFromQueryOrFail("patientId", req);
-  const healthiePracticeId = getFromQueryOrFail("practiceId", req);
+  const { cxId, patientId: healthiePatientId, practiceId: healthiePracticeId } = req.body;
+  
+  // Validate required fields
+  if (!cxId || !healthiePatientId || !healthiePracticeId) {
+    throw new BadRequestError("Missing required fields: cxId, patientId, practiceId");
+  }
  
  updateHealthiePatientQuickNotes({
    cxId,
    healthiePracticeId,
    healthiePatientId,
  }).catch(processAsyncError(`Healthie updateHealthiePatientQuickNotes for cxId: ${cxId}, patientId: ${healthiePatientId}`));
  return res.sendStatus(httpStatus.OK);
})
packages/shared/src/interface/external/ehr/healthie/event.ts (2)

3-8: Consider adding additional validation for event data.

The current schema correctly defines the basic structure for appointment created events, but consider enhancing validation:

  1. If resource_id is expected to be a UUID or specific format, add pattern validation
  2. If resource_id_type has a limited set of valid values, use an enum
  3. Consider adding documentation comments explaining these fields
export const healthieAppointmentCreatedEventSchema = z.object({
-  resource_id: z.coerce.string(),
+  resource_id: z.coerce.string().min(1, "Resource ID cannot be empty"),
-  resource_id_type: z.string(),
+  resource_id_type: z.enum(["appointment_id", "external_id"], {
+    errorMap: () => ({ message: "resource_id_type must be 'appointment_id' or 'external_id'" }),
+  }),
  event_type: z.literal("appointment.created"),
});

10-15: Apply consistent validation patterns.

For consistency, apply the same validation enhancements to the patient created event schema:

export const healthiePatientCreatedEventSchema = z.object({
-  resource_id: z.coerce.string(),
+  resource_id: z.coerce.string().min(1, "Resource ID cannot be empty"),
-  resource_id_type: z.string(),
+  resource_id_type: z.enum(["patient_id", "external_id"], {
+    errorMap: () => ({ message: "resource_id_type must be 'patient_id' or 'external_id'" }),
+  }),
  event_type: z.literal("patient.created"),
});

Also consider creating a union type for when you need to handle multiple event types:

export type HealthieEvent = HealthieAppointmentCreatedEvent | HealthiePatientCreatedEvent;
packages/core/src/util/webhook.ts (1)

58-59: Consider using a more specific type than any.

While the ESLint disable comment acknowledges the use of any, consider using a more specific type for better type safety, such as unknown or a union type of expected payload structures.

-  body: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+  body: unknown; // More type-safe than any
packages/api/src/routes/ehr/healthie/patient-webhook.ts (1)

35-40: Consider extracting the error message to a constant.

Since the same error might be thrown in multiple places (e.g., in the appointment-webhook.ts file as well), consider extracting the error message to a shared constant.

+ const HEALTHIE_MAPPINGS_NOT_FOUND_ERROR = "Healthie secondary mappings not found";

// Later in code
- throw new MetriportError("Healthie secondary mappings not found", undefined, {
+ throw new MetriportError(HEALTHIE_MAPPINGS_NOT_FOUND_ERROR, undefined, {
    externalId: healthiePracticeId,
    source: EhrSources.healthie,
  });
packages/infra/lib/ehr-nested-stack.ts (1)

378-425: Consider refactoring setup methods to reduce duplication.

There's significant code duplication between setupHealthieLinkPatient, setupElationLinkPatient, and other setup methods. Consider extracting common functionality to a shared method that can be parameterized.

+ private setupEhrLinkPatient(ehrName: string, settings: QueueAndLambdaSettings, ownProps: {
+   lambdaLayers: LambdaLayers;
+   vpc: ec2.IVpc;
+   envType: EnvType;
+   sentryDsn: string | undefined;
+   alarmAction: SnsAction | undefined;
+ }): { lambda: Lambda; queue: Queue } {
+   const { lambdaLayers, vpc, envType, sentryDsn, alarmAction } = ownProps;
+   const { name, entry, lambda: lambdaSettings, queue: queueSettings, 
+     eventSource: eventSourceSettings, waitTime } = settings;
+
+   const queue = createQueue({
+     ...queueSettings,
+     stack: this,
+     name,
+     fifo: true,
+     createDLQ: true,
+     lambdaLayers: [lambdaLayers.shared],
+     envType,
+     alarmSnsAction: alarmAction,
+   });
+
+   const lambda = createLambda({
+     ...lambdaSettings,
+     stack: this,
+     name,
+     entry,
+     envType,
+     envVars: {
+       // API_URL set on the api-stack after the OSS API is created
+       WAIT_TIME_IN_MILLIS: waitTime.toMilliseconds().toString(),
+       ...(sentryDsn ? { SENTRY_DSN: sentryDsn } : {}),
+     },
+     layers: [lambdaLayers.shared],
+     vpc,
+     alarmSnsAction: alarmAction,
+   });
+
+   lambda.addEventSource(new SqsEventSource(queue, eventSourceSettings));
+
+   return { lambda, queue };
+ }

// Then in the specific setup methods:
private setupHealthieLinkPatient(ownProps: {...}): { lambda: Lambda; queue: Queue } {
  return this.setupEhrLinkPatient("healthie", settings().healthieLinkPatient, ownProps);
}
packages/api/src/external/ehr/shared.ts (1)

159-177: getLookForwardTimeRangeWithOffset – possible off-by-logic & missing documentation

The function returns
startRange = now + offset,
endRange = now + lookForward.

If offset > lookForward we throw (good), but for the common case the window length shrinks to lookForward - offset instead of the full lookForward.
If the intention was a full lookForward window starting at offset, endRange should be now + offset + lookForward.

At minimum please add a JSDoc comment explaining the expected semantics and confirm the current behaviour is desired.

packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)

10-18: Tighten the schema: constrain event_type to known values

event_type is currently z.string().nullable(), which silently allows typos or future, unsupported values.
Restricting it to the SubscriptionResource union gives us compile-time and run-time safety.

-  event_type: z.string().nullable(),
+  event_type: z.enum(subscriptionResources).nullable(),

Apply the same change to subscriptionSchema (line 16) and webhookEventSchema (line 11) for consistency.

packages/core/src/external/ehr/healthie/index.ts (1)

51-56: Redundant Axios instance creation

axiosInstance is created twice—once in the constructor (line 54) and again in initialize() (line 77).
The first instance is thrown away, wasting a tiny bit of memory and making the constructor do perf-irrelevant work that could hide future bugs.

-    this.axiosInstance = axios.create({});
     this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`;
packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)

30-37: Sanitise header types before signature verification

req.headers can contain string | string[] | undefined values, but verifySignature expects a flat Record<string,string>.
Casting hides a potential runtime error where an array sneaks through and .split("=") (inside verifySignature) blows up.

-      headers: req.headers as Record<string, string>,
+      headers: Object.fromEntries(
+        Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v ?? ""])
+      ),
packages/shared/src/interface/external/ehr/healthie/patient.ts (1)

3-10: Make address line 2 & country fields optional

Real-world data often omits line2 and sometimes country. Marking them as required will cause valid payloads to fail schema validation and abort ingest.

-  line2: z.string(),
+  line2: z.string().nullable().optional(),
-  country: z.string(),
+  country: z.string().nullable().optional(),

Consider also making the other address fields nullable/optional if Healthie allows them to be blank.

packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)

193-199: Rename local variable for clarity

ehrDashUrl actually holds the final patient link, not the dash base URL, which can mislead future readers.

-  const ehrDashUrl = await createHealthiePatientLink({ healthiePracticeId, healthiePatientId });
+  const patientLink = await createHealthiePatientLink({ healthiePracticeId, healthiePatientId });
   const api = healthieApi ?? (await createHealthieClient({ cxId, practiceId: healthiePracticeId }));
   await api.updatePatientQuickNotesWithLink({
     cxId,
     patientId: healthiePatientId,
-    link: ehrDashUrl,
+    link: patientLink,
   });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a4c4246 and 598722e.

📒 Files selected for processing (54)
  • packages/api/src/external/ehr/elation/command/sync-patient.ts (2 hunks)
  • packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1 hunks)
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1 hunks)
  • packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts (1 hunks)
  • packages/api/src/external/ehr/healthie/command/sync-patient.ts (1 hunks)
  • packages/api/src/external/ehr/healthie/shared.ts (1 hunks)
  • packages/api/src/external/ehr/shared.ts (8 hunks)
  • packages/api/src/routes/ehr/athenahealth/patient.ts (2 hunks)
  • packages/api/src/routes/ehr/elation/auth/middleware.ts (1 hunks)
  • packages/api/src/routes/ehr/elation/patient.ts (2 hunks)
  • packages/api/src/routes/ehr/healthie/appointment-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/auth/middleware.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/patient-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/patient.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/routes/dash.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/routes/webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/index.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/athenahealth/patient.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/elation/patient.ts (2 hunks)
  • packages/api/src/routes/internal/ehr/healthie/index.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/healthie/patient.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/healthie/secret-key.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/index.ts (1 hunks)
  • packages/api/src/routes/internal/index.ts (2 hunks)
  • packages/api/src/routes/internal/jwt-token/healthie.ts (1 hunks)
  • packages/api/src/routes/internal/jwt-token/index.ts (1 hunks)
  • packages/api/src/shared/config.ts (1 hunks)
  • packages/core/src/external/ehr/api/link-patient.ts (3 hunks)
  • packages/core/src/external/ehr/athenahealth/index.ts (2 hunks)
  • packages/core/src/external/ehr/elation/index.ts (4 hunks)
  • packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts (2 hunks)
  • packages/core/src/external/ehr/healthie/index.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1 hunks)
  • packages/core/src/external/ehr/shared.ts (2 hunks)
  • packages/core/src/util/config.ts (1 hunks)
  • packages/core/src/util/webhook.ts (2 hunks)
  • packages/infra/config/env-config.ts (1 hunks)
  • packages/infra/lib/api-stack.ts (3 hunks)
  • packages/infra/lib/api-stack/api-service.ts (5 hunks)
  • packages/infra/lib/ehr-nested-stack.ts (6 hunks)
  • packages/infra/lib/secrets-stack.ts (1 hunks)
  • packages/shared/src/domain/secrets.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/elation/index.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/appointment.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/event.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/index.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/patient.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/healthie/subscription.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/source.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.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...

**/*.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
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/api/src/routes/ehr/elation/auth/middleware.ts
  • packages/shared/src/interface/external/ehr/source.ts
  • packages/core/src/util/config.ts
  • packages/api/src/routes/internal/jwt-token/index.ts
  • packages/shared/src/domain/secrets.ts
  • packages/shared/src/interface/external/ehr/elation/index.ts
  • packages/api/src/routes/internal/ehr/healthie/index.ts
  • packages/api/src/routes/ehr/elation/patient.ts
  • packages/api/src/external/ehr/elation/command/sync-patient.ts
  • packages/infra/lib/secrets-stack.ts
  • packages/api/src/routes/internal/ehr/index.ts
  • packages/api/src/shared/config.ts
  • packages/api/src/routes/ehr/healthie/routes/webhook.ts
  • packages/api/src/routes/internal/ehr/elation/patient.ts
  • packages/infra/config/env-config.ts
  • packages/api/src/routes/ehr/athenahealth/patient.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/elation/index.ts
  • packages/api/src/routes/internal/ehr/healthie/secret-key.ts
  • packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts
  • packages/api/src/routes/ehr/healthie/routes/dash.ts
  • packages/api/src/routes/internal/index.ts
  • packages/api/src/routes/ehr/healthie/appointment-webhook.ts
  • packages/api/src/routes/ehr/healthie/patient-webhook.ts
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts
  • packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
  • packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
  • packages/api/src/routes/internal/jwt-token/healthie.ts
  • packages/api/src/routes/internal/ehr/healthie/patient.ts
  • packages/shared/src/interface/external/ehr/healthie/jwt-token.ts
  • packages/api/src/routes/internal/ehr/athenahealth/patient.ts
  • packages/shared/src/interface/external/ehr/healthie/index.ts
  • packages/api/src/routes/ehr/healthie/patient.ts
  • packages/api/src/external/ehr/shared.ts
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
  • packages/infra/lib/api-stack/api-service.ts
  • packages/shared/src/interface/external/ehr/healthie/event.ts
  • packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts
  • packages/core/src/external/ehr/api/link-patient.ts
  • packages/core/src/util/webhook.ts
  • packages/api/src/routes/ehr/index.ts
  • packages/infra/lib/ehr-nested-stack.ts
  • packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts
  • packages/api/src/routes/ehr/healthie/auth/middleware.ts
  • packages/core/src/external/ehr/healthie/index.ts
  • packages/api/src/external/ehr/healthie/shared.ts
  • packages/shared/src/interface/external/ehr/healthie/appointment.ts
  • packages/shared/src/interface/external/ehr/healthie/patient.ts
  • packages/infra/lib/api-stack.ts
  • packages/shared/src/interface/external/ehr/healthie/subscription.ts
  • packages/api/src/external/ehr/healthie/command/sync-patient.ts
🧠 Learnings (1)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/core/src/external/ehr/api/sync-patient.ts:16-55
Timestamp: 2025-03-11T20:42:46.516Z
Learning: In the patient synchronization architecture, the flow follows this pattern: (1) `ehr-sync-patient-cloud.ts` sends messages to an SQS queue, (2) the `ehr-sync-patient` Lambda consumes these messages, and (3) the Lambda uses the `syncPatient` function to make the API calls to process the patient data.
🧬 Code Graph Analysis (19)
packages/core/src/util/config.ts (1)
packages/api/src/shared/config.ts (1)
  • getEnvVarOrFail (14-14)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (4)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
  • HealthieLinkPatientHandler (7-9)
packages/core/src/util/config.ts (1)
  • Config (9-198)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
  • HealthieLinkPatientLocal (6-22)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
  • HealthieLinkPatientCloud (6-26)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
  • subscriptionResources (3-3)
packages/api/src/routes/ehr/healthie/appointment-webhook.ts (8)
packages/api/src/routes/helpers/request-logger.ts (1)
  • requestLogger (10-63)
packages/api/src/routes/util.ts (3)
  • asyncHandler (10-28)
  • getCxIdOrFail (140-144)
  • getFromQueryOrFail (61-65)
packages/shared/src/interface/external/ehr/healthie/event.ts (1)
  • healthieAppointmentCreatedEventSchema (3-7)
packages/api/src/command/mapping/cx.ts (1)
  • getCxMappingOrFail (54-63)
packages/shared/src/index.ts (1)
  • MetriportError (40-40)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
  • healthieSecondaryMappingsSchema (9-19)
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
  • getHealthiePatientFromAppointment (11-24)
packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
  • updateHealthiePatientQuickNotes (187-200)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (3)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (2)
  • HealthieLinkPatientHandler (7-9)
  • ProcessLinkPatientRequest (1-5)
packages/core/src/external/ehr/api/link-patient.ts (1)
  • linkPatient (17-47)
packages/shared/src/index.ts (1)
  • sleep (13-13)
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
packages/api/src/external/ehr/healthie/shared.ts (1)
  • createHealthieClient (101-110)
packages/api/src/routes/internal/jwt-token/healthie.ts (3)
packages/api/src/routes/helpers/request-logger.ts (1)
  • requestLogger (10-63)
packages/api/src/routes/util.ts (2)
  • asyncHandler (10-28)
  • getAuthorizationToken (181-187)
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (2)
  • healthieDashSource (4-4)
  • healthieDashJwtTokenDataSchema (5-9)
packages/api/src/routes/internal/ehr/healthie/patient.ts (5)
packages/api/src/routes/helpers/request-logger.ts (1)
  • requestLogger (10-63)
packages/api/src/routes/util.ts (3)
  • asyncHandler (10-28)
  • getFromQueryOrFail (61-65)
  • getFromQueryAsBoolean (84-86)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)
  • processPatientsFromAppointments (45-169)
packages/core/src/util/error/shared.ts (1)
  • processAsyncError (56-64)
packages/api/src/external/ehr/healthie/command/sync-patient.ts (2)
  • syncHealthiePatientIntoMetriport (47-104)
  • updateHealthiePatientQuickNotes (187-200)
packages/api/src/routes/ehr/healthie/patient.ts (3)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)
  • tokenEhrPatientIdQueryParam (19-19)
packages/api/src/routes/util.ts (4)
  • asyncHandler (10-28)
  • getCxIdOrFail (140-144)
  • getFrom (131-133)
  • getFromQueryOrFail (61-65)
packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
  • syncHealthiePatientIntoMetriport (47-104)
packages/api/src/external/ehr/shared.ts (7)
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (2)
  • healthieDashSource (4-4)
  • HealthieDashJwtTokenData (10-10)
packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (2)
  • AthenaSecondaryMappings (9-9)
  • athenaSecondaryMappingsSchema (3-8)
packages/shared/src/interface/external/ehr/elation/cx-mapping.ts (2)
  • ElationSecondaryMappings (18-18)
  • elationSecondaryMappingsSchema (9-17)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)
  • HealthieSecondaryMappings (20-20)
  • healthieSecondaryMappingsSchema (9-19)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/shared/src/common/date.ts (1)
  • buildDayjs (70-72)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
packages/infra/lib/api-stack/api-service.ts (1)
packages/infra/lib/shared/sqs.ts (1)
  • provideAccessToQueue (219-233)
packages/core/src/external/ehr/api/link-patient.ts (1)
packages/core/src/external/ehr/api/api-shared.ts (1)
  • ApiBaseParams (3-9)
packages/core/src/util/webhook.ts (1)
packages/shared/src/index.ts (1)
  • MetriportError (40-40)
packages/api/src/routes/ehr/index.ts (1)
packages/api/src/routes/middlewares/auth.ts (1)
  • checkMAPIAccess (46-65)
packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts (5)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (2)
  • SubscriptionResource (4-4)
  • SubscriptionWithSignatureSecret (28-28)
packages/api/src/command/mapping/cx.ts (2)
  • getCxMappingOrFail (54-63)
  • setSecondaryMappingsOnCxMappingById (147-164)
packages/shared/src/index.ts (1)
  • MetriportError (40-40)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
  • healthieSecondaryMappingsSchema (9-19)
packages/api/src/external/ehr/healthie/shared.ts (1)
  • createHealthieClient (101-110)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (7)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
  • isSubscriptionResource (5-7)
packages/api/src/external/ehr/healthie/shared.ts (1)
  • getHealthieSecretKeyInfo (112-139)
packages/core/src/util/webhook.ts (1)
  • verifySignature (76-95)
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)
  • healthieDashSource (4-4)
packages/api/src/routes/util.ts (1)
  • getAuthorizationToken (181-187)
packages/shared/src/common/date.ts (1)
  • buildDayjs (70-72)
packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
  • shortDurationTokenDuration (37-37)
packages/core/src/external/ehr/healthie/index.ts (6)
packages/core/src/util/config.ts (1)
  • Config (9-198)
packages/core/src/external/ehr/shared.ts (3)
  • MakeRequestParamsInEhr (79-82)
  • makeRequest (84-202)
  • formatDate (55-61)
packages/shared/src/index.ts (2)
  • MetriportError (40-40)
  • NotFoundError (41-41)
packages/shared/src/interface/external/ehr/healthie/patient.ts (7)
  • Patient (23-23)
  • PatientGraphql (29-29)
  • patientGraphqlSchema (24-28)
  • PatientQuickNotesGraphql (41-41)
  • patientQuickNotesGraphqlSchema (36-40)
  • PatientUpdateQuickNotesGraphql (50-50)
  • patientUpdateQuickNotesGraphqlSchema (43-49)
packages/shared/src/interface/external/ehr/healthie/appointment.ts (5)
  • AppointmentWithAttendee (10-12)
  • AppointmentListResponseGraphql (30-30)
  • appointmentListResponseGraphqlSchema (27-29)
  • AppointmentGetResponseGraphql (21-21)
  • appointmentGetResponseGraphqlSchema (18-20)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (5)
  • Subscription (24-24)
  • SubscriptionResource (4-4)
  • SubscriptionWithSignatureSecret (28-28)
  • SubscriptionWithSignatureSecretGraphql (36-38)
  • subscriptionWithSignatureSecretGraphqlSchema (29-35)
packages/api/src/external/ehr/healthie/shared.ts (9)
packages/shared/src/index.ts (4)
  • BadRequestError (39-39)
  • toTitleCase (15-15)
  • MetriportError (40-40)
  • NotFoundError (41-41)
packages/api/src/shared/config.ts (2)
  • getHealthieEnv (356-358)
  • Config (16-366)
packages/api/src/external/ehr/shared.ts (2)
  • EhrPerPracticeParams (179-179)
  • EhrEnvAndApiKey (63-66)
packages/core/src/external/ehr/healthie/index.ts (2)
  • HealthieEnv (40-40)
  • isHealthieEnv (41-43)
packages/core/src/util/config.ts (1)
  • Config (9-198)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
  • SubscriptionResource (4-4)
packages/api/src/command/mapping/cx.ts (1)
  • getCxMappingOrFail (54-63)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
  • healthieSecondaryMappingsSchema (9-19)
packages/shared/src/domain/secrets.ts (1)
  • cxApiKeyMapSecretSchema (4-4)
packages/shared/src/interface/external/ehr/healthie/appointment.ts (1)
packages/api/src/external/ehr/shared.ts (1)
  • Appointment (127-131)
🪛 Biome (1.9.4)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts

[error] 60-60: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
🔇 Additional comments (74)
packages/api/src/routes/ehr/elation/auth/middleware.ts (1)

1-1: Import ordering improvement.

Moving the verifyWebhookSignatureEd25519 import to the top maintains consistency with other EHR middleware modules, including the new Healthie implementation. This change doesn't affect functionality but improves code organization.

packages/shared/src/domain/secrets.ts (1)

4-4: Good addition of Healthie API key schema.

The new cxApiKeyMapSecretSchema provides type validation for Healthie API keys, supporting the integration's API client creation and webhook subscription functionality. This follows the same pattern as the existing schema while representing a distinct semantic entity.

packages/shared/src/interface/external/ehr/source.ts (1)

5-5: Good implementation of Healthie EHR source.

Adding Healthie to the EhrSources enum properly integrates it into the type system. The implementation is clean as the ehrSources array and EhrSource type automatically incorporate the new value without requiring additional changes.

packages/api/src/routes/internal/jwt-token/index.ts (1)

5-5: Clean integration of Healthie JWT token routes.

The addition of Healthie JWT token routes follows the established pattern for other EHR integrations, maintaining consistency in the codebase. This change properly integrates Healthie token management into the API's routing structure without modifying existing functionality.

Also applies to: 13-13

packages/core/src/util/config.ts (1)

180-182: Implementation follows established patterns.

The method implementation correctly follows the project's pattern for environment variable retrieval and is appropriately positioned near similar EHR-related methods.

packages/api/src/external/ehr/elation/command/sync-patient.ts (2)

12-12: LGTM: Necessary dayjs plugin import.

Adding the duration plugin import is necessary for the token duration functionality in this file.


28-28: Correctly extends dayjs with the duration plugin.

The extension is properly placed before using duration-related functionality in the constants below.

packages/infra/lib/secrets-stack.ts (1)

65-65: Correctly adds Healthie secrets following the established pattern.

The implementation properly integrates Healthie secrets into the AWS Secrets Manager provisioning flow, consistent with how other EHR integrations are handled.

packages/api/src/routes/internal/ehr/healthie/index.ts (1)

1-10: Well-structured router with clean separation of concerns.

The implementation follows Express best practices with a modular approach, separating patient and secret-key routes into their own sub-routers. The code is concise and follows the project's functional programming style.

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

102-104: Improved header case-insensitivity handling

This change properly handles case-insensitivity for Content-Type headers, which is good since HTTP headers are case-insensitive according to HTTP specifications, but implementations can vary.


118-118: Good defensive URL handling

The defensive check to only include the URL if it's not an empty string prevents potential issues with empty URLs in Axios requests.

packages/api/src/routes/internal/ehr/index.ts (1)

5-5: LGTM - Consistent integration of Healthie routes

The additions follow the established pa 6D47 ttern for integrating EHR providers, maintaining consistency with the existing implementation for other providers (athena, elation, canvas).

Also applies to: 12-12

packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts (1)

2-3: LGTM - Good abstraction for multi-EHR support

The changes effectively refactor the linkPatient function to support multiple EHR sources dynamically, improving code reusability and maintainability. Explicitly specifying EhrSources.elation makes the code more self-documenting.

Also applies to: 15-15

packages/api/src/routes/ehr/healthie/routes/webhook.ts (1)

1-10: Clean webhook route implementation

The implementation follows good patterns with:

  • Using express-promise-router for automatic promise handling
  • Clear organization with dedicated sub-routers for different webhook types
  • Thoughtful route structure using path parameters
packages/core/src/external/ehr/athenahealth/index.ts (2)

178-178: Improved HTTP header case consistency.

Updated the "Content-Type" header to use proper capitalization, which follows standard HTTP header conventions.


211-211: Standardized HTTP header capitalization.

Consistent with the earlier change, updated the "Content-Type" header to use proper capitalization in the axios instance configuration.

packages/shared/src/interface/external/ehr/elation/index.ts (1)

6-7: Expanded Elation exports to include cx-mapping and event modules.

Added exports for cx-mapping and event modules to enable centralized imports from the Elation interface. This aligns with the pattern used by other EHR integrations including the new Healthie integration.

packages/core/src/external/ehr/elation/index.ts (4)

100-100: Standardized HTTP header capitalization.

Updated the "Content-Type" header to use proper capitalization in the token request, which follows standard HTTP header conventions.


131-131: Improved HTTP header case consistency.

Standardized the "Content-Type" header capitalization in the axios instance initialization.


176-176: Added explicit Content-Type header for PATCH request.

Explicitly specified the "Content-Type" header for the PATCH request to update patient metadata, ensuring the server correctly interprets the request body as JSON.


229-229: Added explicit Content-Type header for POST request.

Added the "Content-Type" header for the problem creation POST request, ensuring proper content negotiation with the Elation API.

packages/infra/config/env-config.ts (1)

260-265: Added Healthie configuration to support new EHR integration.

Added the configuration block for Healthie in the ehrIntegration section with environment and API key map settings. This supports the Healthie integration being introduced in this PR.

The structure follows the pattern established for other EHR integrations (athenaHealth, elation, canvas) with appropriate environment and secrets configuration.

packages/api/src/shared/config.ts (1)

356-361: Implementation correctly follows the pattern of other EHR configurations.

The addition of getHealthieEnv and getHealthieApiKeyMap methods is consistent with the existing pattern for other EHR providers (like Athena, Elation, Canvas) and follows the codebase's convention for environment variable access.

packages/api/src/routes/ehr/elation/patient.ts (2)

18-18: Documentation improvement is accurate and helpful.

The added documentation for the practiceId query parameter clearly specifies its purpose as "The ID of Elation Practice", which matches the implementation that uses getFromQueryOrFail("practiceId", req) in line 29.


44-44: Documentation improvement is accurate and consistent.

The added documentation for the practiceId query parameter in the POST route matches what was added for the GET route, maintaining consistency between the two endpoint definitions.

packages/api/src/routes/ehr/index.ts (3)

17-22: Clear and consistent implementation of middleware imports.

The import statements for Healthie middleware and routes follow the same pattern used for other EHR providers, using appropriate naming conventions to ensure clarity of purpose for each component.


29-29: Route registration follows established pattern.

The Healthie dashboard route is correctly registered with the same middleware chain as other EHR providers: first the customer ID processing middleware, then the MAPI access check, and finally the route handler.


33-33: Webhook route registration is properly implemented.

The Healthie webhook route is correctly registered with the appropriate middleware chain, consistent with the pattern used for other EHR providers' webhook routes.

packages/shared/src/interface/external/ehr/healthie/index.ts (1)

1-6: Well-structured exports follow the barrel pattern.

The module correctly uses the barrel pattern to re-export all entities from related Healthie interface modules, making it easy to import all Healthie-related types with a single import statement. This approach is clean and maintainable.

packages/api/src/routes/ehr/athenahealth/patient.ts (2)

16-17: JSDoc improvement - query parameter documentation added.

The documentation now properly includes these athenahealth-specific query parameters, which improves code clarity and developer experience.


44-45: JSDoc improvement - query parameter documentation added.

The documentation for the POST endpoint now correctly specifies the required query parameters, matching the implementation.

packages/api/src/routes/internal/ehr/athenahealth/patient.ts (1)

71-75: Documentation accurately reflects the implementation parameters.

The JSDoc comments now properly document all query parameters used in the route handler: cxId, patientId, practiceId, departmentId, and triggerDq. This matches the parameters extracted in the implementation (lines 82-86).

packages/api/src/routes/internal/index.ts (2)

35-36: Good renaming for clearer function imports.

Renaming the imports with more specific names (subscribeToElationWebhooks and subscribeToHealthieWebhooks) makes the code more readable and maintainable by clearly indicating which EHR system each function applies to.


331-334: Feature addition: Healthie webhook subscription support.

The code now handles webhook subscriptions for both Elation and Healthie sources, properly extending the system to support the new Healthie integration. The conditional logic is clear and follows the same pattern as the existing Elation implementation.

packages/api/src/routes/internal/ehr/elation/patient.ts (2)

37-41: Documentation accurately reflects implementation parameters.

The JSDoc comments now correctly document all query parameters used in the route handler: cxId, patientId, practiceId, and the optional triggerDq flag. This matches how the parameters are extracted and used in the implementation (lines 47-56).


65-68: Documentation accurately reflects implementation parameters.

The JSDoc comments now properly document all query parameters used in the patient linking route handler. This improves code clarity and consistency across similar endpoints.

packages/infra/lib/api-stack.ts (3)

416-418: Correct integration of Healthie resources.

The Healthie link patient queue and lambda are properly destructured from the EHR nested stack, following the same pattern as other EHR integrations.


545-545: Queue properly included in API service.

The Healthie link patient queue is correctly added to the API service configuration.


640-640: Lambda correctly added to API URL recipients.

The Healthie link patient lambda is properly added to the list of lambdas that receive the API_URL environment variable.

packages/api/src/routes/internal/ehr/healthie/secret-key.ts (1)

20-32: Well-structured route implementation with proper validation.

The endpoint correctly validates inputs and handles the secret key retrieval with appropriate error handling. The use of asyncHandler ensures that async errors are properly captured and propagated.

packages/api/src/routes/ehr/healthie/routes/dash.ts (1)

1-37: Well-structured router with proper middleware organization.

The implementation follows the established patterns used in other EHR integrations. The router effectively organizes routes for patient data, medical documents, and settings with appropriate middleware chains for each endpoint.

packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)

1-9: Clean type and interface definition for patient linking.

The exported type ProcessLinkPatientRequest and interface HealthieLinkPatientHandler provide a clear contract for implementing patient linking functionality. This follows TypeScript best practices with proper naming conventions and clear type definitions.

packages/infra/lib/api-stack/api-service.ts (1)

112-112: Properly integrates Healthie queue resources into the API service.

The changes correctly add the Healthie link patient queue to the API service by:

  1. Adding the queue parameter to the function signature
  2. Including the queue URL in environment variables
  3. Setting up the Healthie environment configuration
  4. Granting the necessary SQS send permissions to the task role

These changes maintain consistency with the existing patterns used for other EHR integrations.

Also applies to: 154-154, 289-289, 339-339, 452-456

packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (2)

4-9: Good type definition with clear parameters

The type definition is properly structured with clear parameter names and types, following the TypeScript guidelines. The optional API parameter allows for dependency injection, which is good for testability.


11-16: Function signature follows best practices

The function signature properly destructures the parameters object, which aligns with the coding guidelines of preferring deconstructing parameters for functions instead of multiple parameters.

packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (2)

6-8: Class implementation follows interface contract

The class correctly implements the HealthieLinkPatientHandler interface and declares a private SQS client property.


9-15: Good constructor with fallback initialization

The constructor properly initializes the SQS client with fallback to default AWS region if not provided. The use of the nullish coalescing operator aligns with the coding guidelines.

packages/core/src/external/ehr/api/link-patient.ts (5)

7-7: Good type definition using Omit

The type definition uses Omit to leverage the existing ApiBaseParams type, which follows the DRY principle.


12-12: Updated JSDoc to reflect the parameter change

The JSDoc is properly updated to include documentation for the new ehr parameter.


18-18: Function parameter update maintains backward compatibility

The function now accepts an ehr parameter, making it more generic and reusable for different EHR sources.


30-30: Dynamic URL construction based on EHR source

The URL is now dynamically constructed based on the EHR parameter, which is more flexible than the previous hardcoded approach.


39-44: Enhanced error metadata with EHR source

Error handling now includes the EHR source in the error metadata, which improves debuggability. The context is also more generic, which aligns with the function's broader scope.

packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (3)

4-4: Good use of constants for EHR source

Using a constant for the EHR source ensures consistency across the codebase and makes maintenance easier if the source name changes.


5-9: Well-structured schema with appropriate validations

The schema is well-defined with appropriate validations for the required fields. The use of z.literal for the source field ensures type safety.


10-10: Type definition inferred from schema

The type is properly inferred from the Zod schema, which ensures type safety and consistency between runtime validation and compile-time types.

packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)

4-7: Webhook schema implementation looks good.

The webhook schema defines the required fields for Healthie webhooks with appropriate validation.

packages/core/src/util/webhook.ts (3)

35-45: HMAC implementation looks good.

The implementation of the getSigningKey function correctly imports a raw secret key as a CryptoKey for HMAC-SHA256 signing according to the Healthie documentation.


47-66: Data signing construction follows security best practices.

The function correctly extracts the content digest from headers and constructs the data to sign with all required components for proper HMAC verification.


68-74: Hex encoding implementation is clean and efficient.

The signature generation function properly uses the Web Crypto API and converts the result to a hex string using a functional approach.

packages/api/src/routes/ehr/healthie/patient-webhook.ts (4)

1-14: Import organization looks good.

The imports are well-organized, grouped by external dependencies and internal modules, which enhances readability.


17-22: Clear JSDoc for the endpoint.

The documentation clearly describes the endpoint's purpose and expected response.


23-62: Route handler implementation follows best practices.

The route handler correctly:

  • Uses middleware for parameter handling, request logging, and error handling
  • Validates input parameters and the request body
  • Checks configuration flags to determine processing behavior
  • Returns appropriate HTTP responses

50-59: Validation between quick notes update and patient processing.

The code allows for updating quick notes even when patient processing is disabled. Confirm this is the intended behavior as it might lead to inconsistent states if the quick notes contain references to processing that didn't occur.

packages/infra/lib/ehr-nested-stack.ts (4)

18-18: Wait time constant follows existing pattern.

The new constant waitTimeHealthieLinkPatient aligns with the existing constants used for rate limiting.


26-26: New Healthie link patient configuration follows existing structure.

The Healthie link patient settings follow the same structure as existing configurations, which maintains consistency in the codebase.

Also applies to: 73-93, 166-166


186-187: Property declarations align with existing patterns.

The new properties healthieLinkPatientLambda and healthieLinkPatientQueue are correctly declared as readonly, following the pattern of existing properties.


235-243: Constructor setup follows existing pattern.

The Healthie link patient setup in the constructor correctly follows the established pattern for other EHR integrations.

packages/api/src/routes/ehr/healthie/appointment-webhook.ts (4)

1-15: Import organization aligns with the patient webhook.

The imports follow the same organization pattern as the patient webhook file, maintaining consistency.


16-23: Router and documentation follow consistent patterns.

The router setup and JSDoc follow the same structure as other webhook endpoints.


24-69: Route handler structure is consistent with other webhook handlers.

The implementation follows the same pattern as the patient webhook handler:

  • Uses the same middleware
  • Extracts and validates parameters
  • Processes the webhook based on configuration flags
  • Returns appropriate HTTP responses

56-58: Code structure allows for early returns.

The use of early returns when processing should be skipped makes the code more readable and reduces nesting.

Also applies to: 42-45

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

51-53: Zero-delay between practice batches may overload the Healthie API

delayBetweenPracticeBatches was reduced to 0 seconds. With parallelPractices = 10, every batch against every practice will fire at once.
Healthie’s rate-limits are unknown here, but even Athena/Canvas integrations keep at least a small pause to avoid 429s.

Consider keeping a modest delay (e.g. 250-500 ms) or making it configurable per-EHR so we can quickly tune it without a code change.

packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)

121-123: Deduplication key might be too coarse

uniqBy(allAppointments, "patientId") removes duplicates solely on patientId.
If a patient can appear in multiple practices (or multiple cxIds) we will process only the first occurrence, potentially skipping necessary linking/sync for the other practices.

Verify whether Healthie patient IDs are globally unique across practices; if not, consider a composite key:

uniqBy(allAppointments, a => `${a.practiceId}-${a.patientId}`)
packages/core/src/external/ehr/healthie/index.ts (1)

195-199: TODO is out-of-sync with implementation

The comment says “Remove existingQuickNotes and link from the mutation” but the variable still concatenates scrubbedExistingQuickNotes and linkElement.
Either remove the TODO or adjust the mutation variables to reflect the intended behaviour.

Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.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: 1

♻️ Duplicate comments (2)
packages/core/src/external/ehr/healthie/index.ts (2)

274-282: Preserve all attendees instead of truncating to one

Mapping every appointment to only its first attendee discards valid data and silently corrupts downstream processing if multiple attendees exist.

-    return appointments.map(appointment => {
-      const attendee = appointment.attendees[0];
-      if (!attendee) {
-        throw new MetriportError("Appointment has no attendees", undefined, additionalInfo);
-      }
-      return { ...appointment, attendees: [attendee] };
-    });
+    return appointments.map(appointment => {
+      if (!appointment.attendees?.length) {
+        throw new MetriportError("Appointment has no attendees", undefined, additionalInfo);
+      }
+      // retain the full attendee list
+      return appointment as AppointmentWithAttendee;
+    });

336-352: Fix invalid GraphQL query & variable mismatch

query webhooks() is syntactically invalid—GraphQL operation names should not have empty parentheses, and the $page_size variable you pass is never declared/used inside the query. This will 400-fail at runtime.

-    const query = `query webhooks() {
+    const query = `query webhooks($page_size: Int) {
       webhooks(
         order_by: CREATED_AT_ASC
+        page_size: $page_size
       ) {

If pagination is not actually required, drop both the variable and the variables object instead.

🧹 Nitpick comments (3)
packages/core/src/external/ehr/healthie/index.ts (3)

195-195: Address the TODO comment before merging

There's a TODO comment about removing existingQuickNotes and link from the mutation. Consider addressing this before finalizing the implementation.


176-179: Consider optimizing the regex for scrubbing quick notes

The current regex pattern to remove existing Metriport Integration links is quite general and might match more than intended. Consider making it more specific to your link format.

-    const scrubbedExistingQuickNotes = existingQuickNotes?.replace(
-      /<p><a .*>Metriport Integration<\/a><\/p>/g,
-      ""
-    );
+    const scrubbedExistingQuickNotes = existingQuickNotes?.replace(
+      /<p><a href="[^"]*" target="_blank">Metriport Integration<\/a><\/p>/g,
+      ""
+    );

383-397: Simplify subscription existence check

The current implementation for checking if a subscription exists is quite verbose. Consider simplifying it using Array.some() with a destructured parameter for better readability.

-    if (
-      existingSubscriptions.some(subscription => {
-        const isUrlMatch = subscription.url === url;
-        const isResourceMatch =
-          subscription.event_type === resource ||
-          subscription.webhook_events?.some(event => event.event_type === resource);
-        return isUrlMatch && isResourceMatch;
-      })
-    ) {
+    if (existingSubscriptions.some(({ url: subUrl, event_type, webhook_events }) => 
+      subUrl === url && (
+        event_type === resource || 
+        webhook_events?.some(event => event.event_type === resource)
+      )
+    )) {
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 598722e and aa8c49d.

📒 Files selected for processing (3)
  • packages/api/src/routes/ehr/healthie/auth/middleware.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/routes/webhook.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/src/routes/ehr/healthie/routes/webhook.ts
  • packages/api/src/routes/ehr/healthie/auth/middleware.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.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...

**/*.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
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/core/src/external/ehr/healthie/index.ts
🔇 Additional comments (4)
packages/core/src/external/ehr/healthie/index.ts (4)

80-123: LGTM: Well-structured patient retrieval implementation

The getPatient method is well-structured with appropriate error handling, schema validation, and use of the GraphQL API. The approach to patient data retrieval is done correctly.


125-159: LGTM: Clean implementation of getPatientQuickNotes method

This method follows the established pattern for API interaction with proper error handling, validation, and use of null coalescing for the return value.


161-214: LGTM: Effective link management in updatePatientQuickNotesWithLink

Despite the TODO comment, the implementation correctly handles removing existing Metriport links before adding the new one, preventing duplication.


446-480: LGTM: Well-abstracted utility methods

The makeRequest and formatDate utility methods are well abstracted and follow the coding guidelines by being small, deterministic functions with clear inputs and outputs.

Thomas Yopes added 2 commits April 30, 2025 14:06
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Copy link
@coderabbitai coderabbitai bot 10000 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/healthie/index.ts (1)

336-351: ⚠️ Potential issue

Fix invalid GraphQL query syntax

The GraphQL query has empty parentheses after the operation name, which is syntactically invalid and will cause runtime errors.

-    const query = `query webhooks() {
+    const query = `query webhooks {
       webhooks(
         order_by: CREATED_AT_ASC
       ) {
🧹 Nitpick comments (5)
packages/core/src/external/ehr/healthie/index.ts (5)

54-56: Optimize constructor and initialize method

The constructor creates an axios instance that's immediately replaced in the initialize method. This creates unnecessary object instantiation.

  private constructor(config: HealthieApiConfig) {
    this.apiKey = config.apiKey;
    this.practiceId = config.practiceId;
-    this.axiosInstance = axios.create({});
    this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`;
  }

257-263: Consider implementing pagination handling

The current implementation has a hard limit of 1000 appointments and doesn't handle scenarios where more appointments exist than the page size allows.

Consider implementing a pagination mechanism to fetch all appointments when there are more than the page size limit. This would make the API more robust for practices with high appointment volumes.

    const variables = {
      startDate: this.formatDate(startAppointmentDate.toISOString()) ?? "",
      endDate: this.formatDate(endAppointmentDate.toISOString()) ?? "",
      offset: 0,
      page_size: 1000,
    };
+    // TODO: Implement pagination to handle more than 1000 appointments

382-397: Improve subscription existence check

The current check for existing subscriptions is complex and could be simplified. Additionally, a warning or status update would be more user-friendly than throwing an error when a subscription already exists.

    const existingSubscriptions = await this.getSubscriptions({ cxId });
-    if (
-      existingSubscriptions.some(subscription => {
-        const isUrlMatch = subscription.url === url;
-        const isResourceMatch =
-          subscription.event_type === resource ||
-          subscription.webhook_events?.some(event => event.event_type === resource);
-        return isUrlMatch && isResourceMatch;
-      })
-    ) {
-      throw new MetriportError(
-        "Subscription already exists for resource",
-        undefined,
-        additionalInfo
-      );
+    const existingSubscription = existingSubscriptions.find(subscription => {
+      const isUrlMatch = subscription.url === url;
+      const isResourceMatch =
+        subscription.event_type === resource ||
+        subscription.webhook_events?.some(event => event.event_type === resource);
+      return isUrlMatch && isResourceMatch;
+    });
+    
+    if (existingSubscription) {
+      debug("Subscription already exists for resource", { subscriptionId: existingSubscription.id });
+      return existingSubscription as SubscriptionWithSignatureSecret;
     }

446-475: Consider adding retry logic for API requests

The makeRequest method doesn't include retry logic for transient failures, which can be common in network requests to external APIs.

Consider adding retry logic with exponential backoff for API requests to handle transient failures more gracefully. This is especially important for webhook operations that are critical for maintaining data synchronization.


29-29: Avoid using Config class directly

Directly using the Config class creates tight coupling and makes testing more difficult.

Consider injecting the API URL through the constructor or create method parameters instead of accessing it directly from the Config class. This would improve testability and flexibility.

-const apiUrl = Config.getApiUrl();

 interface HealthieApiConfig
   extends Omit<ApiConfig, "twoLeggedAuthTokenInfo" | "clientKey" | "clientSecret"> {
   apiKey: string;
   environment: HealthieEnv;
+  apiUrl?: string;
 }

 class HealthieApi {
   private axiosInstance: AxiosInstance;
   private baseUrl: string;
   private apiKey: string;
   private practiceId: string;
+  private apiUrl: string;

   private constructor(config: HealthieApiConfig) {
     this.apiKey = config.apiKey;
     this.practiceId = config.practiceId;
     this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`;
+    this.apiUrl = config.apiUrl ?? Config.getApiUrl();
   }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between aa8c49d and 3e3584e.

📒 Files selected for processing (1)
  • packages/core/src/external/ehr/healthie/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.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...

**/*.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
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/core/src/external/ehr/healthie/index.ts
🧠 Learnings (1)
packages/core/src/external/ehr/healthie/index.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3754
File: packages/core/src/external/ehr/healthie/index.ts:274-282
Timestamp: 2025-04-30T21:07:37.904Z
Learning: In the Healthie integration, appointments are assumed to have one attendee of interest, and the code is designed to extract and work with only the first attendee from each appointment.
🔇 Additional comments (2)
packages/core/src/external/ehr/healthie/index.ts (2)

274-282: Appointment attendee handling implementation

The current implementation assumes appointments have one attendee of interest and extracts only the first attendee from each appointment.

Based on the retrieved learnings, this is an intentional design choice. The code correctly extracts the first attendee and throws an appropriate error if no attendees exist.


324-329: Consistent attendee handling in getAppointment method

Similar to getAppointments, this method extracts only the first attendee from an appointment.

This is consistent with the design choice noted in the retrieved learnings, where appointments are assumed to have one attendee of interest.

Thomas Yopes added 2 commits May 1, 2025 05:45
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.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: 1

🧹 Nitpick comments (4)
packages/lambdas/src/healthie-link-patient.ts (3)

28-28: Replace direct console.log with proper logging utility

According to your coding guidelines, you should "avoid using console.log and console.error in packages other than utils, infra and shared, and try to use out().log instead".

-  console.log(`Running with unparsed body: ${message.body}`);
+  log(`Running with unparsed body: ${message.body}`);

Note: This change requires moving the prefixed log declaration above this line.


33-33: Avoid multi-line logs with JSON.stringify

The coding guidelines specify: "Avoid multi-line logs - don't create multi-line strings when using JSON.stringify()".

-  log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`);
+  log(`Parsed: cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}, waitTimeInMillis ${waitTimeInMillis}`);

32-33: Move log declaration to optimize code structure

The prefixed log declaration should be moved above line 28 to allow its use for all logging operations in this Lambda.

  const parsedBody = parseBody(message.body);
  const { cxId, practiceId, patientId } = parsedBody;

  const log = prefixedLog(`cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`);
-  log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`);
+  log(`Parsed successfully, waitTimeInMillis ${waitTimeInMillis}`);
packages/lambdas/src/shared/ehr.ts (1)

58-60: Consider adding a discriminator property to differentiate EHR sources

The function returns a union type without a clear way to distinguish which EHR system the data belongs to. Consider adding a discriminator property or source indicator to help downstream code safely handle the different types.

export function parseLinkPatient(
  bodyAsJson: LinkPatientPayload
): ElationProcessLinkPatientRequest | HealthieProcessLinkPatientRequest {

You could add a source parameter to the function or consider extending the interface to include a source field:

interface LinkPatientPayload {
  cxId: unknown;
  practiceId: unknown;
  patientId: unknown;
  source?: unknown; // Optional source identifier
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4efbf5d and 6026c54.

📒 Files selected for processing (3)
  • packages/lambdas/src/elation-link-patient.ts (2 hunks)
  • packages/lambdas/src/healthie-link-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/lambdas/src/elation-link-patient.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.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...

**/*.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
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/lambdas/src/healthie-link-patient.ts
  • packages/lambdas/src/shared/ehr.ts
🧬 Code Graph Analysis (1)
packages/lambdas/src/healthie-link-patient.ts (5)
packages/lambdas/src/shared/sqs.ts (1)
  • getSingleMessageOrFail (62-83)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
  • HealthieLinkPatientLocal (6-22)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
  • ProcessLinkPatientRequest (1-5)
packages/shared/src/index.ts (1)
  • MetriportError (40-40)
packages/lambdas/src/shared/ehr.ts (1)
  • parseLinkPatient (58-78)
🔇 Additional comments (3)
packages/lambdas/src/healthie-link-patient.ts (1)

46-49: 🛠️ Refactor suggestion

Add error handling for JSON parse operation

The JSON.parse call could throw an exception if the body isn't valid JSON. Add try/catch to handle this gracefully and provide context in the error message.

-  const bodyAsJson = JSON.parse(bodyString);
+  let bodyAsJson;
+  try {
+    bodyAsJson = JSON.parse(bodyString);
+  } catch (error) {
+    throw new MetriportError("Invalid JSON in message body", error);
+  }
⛔ Skipped due to learnings
Learnt from: thomasyopes
PR: metriport/metriport#3608
File: packages/lambdas/src/ehr-compute-resource-diff-bundles.ts:58-62
Timestamp: 2025-04-23T19:00:49.707Z
Learning: For Lambda functions in the metriport codebase, the team prefers to let the default Lambda error handling catch JSON parsing errors rather than adding explicit try/catch blocks.
packages/lambdas/src/shared/ehr.ts (2)

3-3: LGTM - Good job adding Healthie support

The import for Healthie's ProcessLinkPatientRequest and the type renaming approach keeps the code clean and maintainable while supporting both EHR sources.


52-56: LGTM - Good job generalizing the interface

Renaming from the specific ElationLinkPatientPayload to the generic LinkPatientPayload aligns well with supporting multiple EHR systems.

Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Thomas Yopes added 2 commits May 1, 2025 07:57
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes added this pull request to the merge queue May 1, 2025
Merged via the queue into develop with commit ee751be May 1, 2025
31 checks passed
@thomasyopes thomasyopes deleted the 82-healthie branch May 1, 2025 15:56
@thomasyopes thomasyopes mentioned this pull request May 1, 2025
2 tasks
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
0