Skip to content

Conversation

thomasyopes
Copy link
Contributor

@thomasyopes thomasyopes commented Mar 7, 2025

Ref: #1040

Description

  • instead of syncing patients in real-time, send them to a queue to be processed

Testing

  • Local
    • sync patient works via direct ping
  • Staging
    • sync patients works via queue
  • Sandbox
    • N/A
  • Production
    • sync patients works via queue

Release Plan

  • Merge this

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added new API endpoints for synchronizing patient data from Athenahealth and Elation.
    • Introduced a new endpoint for handling patient data synchronization with Canvas.
    • Implemented a queue-based synchronization system with cloud and local processing capabilities.
  • Infrastructure

    • Enhanced the existing synchronization process with a new handler for improved reliability.
    • Integrated EHR functionalities into the API stack for managing patient synchronization queues.
  • Performance Improvements

    • Increased parallel processing capacity for more efficient patient data synchronization.
    • Streamlined error handling during patient synchronization for better performance.
  • Updates

    • Updated import paths for various modules to reflect new directory structures.
    • Refined type definitions for enhanced type safety in patient synchronization processes.
    • Introduced new constants and types for improved organization and clarity in EHR source handling.
    • Added a new method for retrieving the EHR sync patient queue URL from configuration.
    • Removed outdated JWT token schemas and types for cleaner code structure.

Thomas Yopes added 2 commits March 7, 2025 10:01
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Copy link

coderabbitai bot commented Mar 7, 2025

Walkthrough

The changes update patient synchronization across multiple EHR integrations. The sync functions now rely on a new handler built using a factory method that decides between local and cloud implementations. Error collection and delay handling have been simplified. Additionally, new POST endpoints have been added for athenahealth, elation, and canvas, accompanied by updates to JWT token schemas, middleware, and configuration. Several import paths and type definitions have been reorganized across API, infra, lambda, core, and shared modules.

Changes

File(s) Change Summary
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts Refactored syncPatient to exclude triggerDq parameter, use buildEhrSyncPatientHandler, and remove complex error handling in favor of direct asynchronous execution.
packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts Refactored syncPatient to exclude triggerDq parameter, use buildEhrSyncPatientHandler, and remove complex error handling in favor of direct asynchronous execution.
packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts Refactored syncPatient to exclude triggerDq parameter, use buildEhrSyncPatientHandler, and remove complex error handling in favor of direct asynchronous execution.
packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts Added new POST endpoint for syncing patient data; updated request parameter extraction and error handling using utility functions and middleware.
packages/api/src/routes/ehr-internal/elation/internal/patient.ts Added new POST endpoint for syncing patient data; updated request parameter extraction and error handling using utility functions and middleware.
packages/api/src/routes/ehr-internal/canvas/internal/patient.ts Added new POST endpoint for syncing patient data; updated request parameter extraction and error handling using utility functions and middleware.
packages/infra/lib/api-stack/api-service.ts & packages/core/src/external/ehr/sync-patient/** Introduced a new EHR nested stack with a dedicated SQS queue and Lambda; implemented new sync patient handler classes (EhrSyncPatientLocal, EhrSyncPatientCloud) and a factory (buildEhrSyncPatientHandler).
packages/core/src/util/config.ts, packages/api/src/external/ehr/shared.ts, packages/api/src/command/mapping/*, packages/api/src/domain/* Updated configuration, added new constants (e.g. delayBetweenPatientBatches), increased parallelism value, and restructured import paths, removing unused enums in favor of centralized type sources.
packages/api/src/routes/ehr/athenahealth/auth/* Updated JWT token routes and middleware to use revised schema constants and source variables (e.g. athenaDashSource) for consistent token validation.
packages/api/src/routes/ehr/canvas/auth/* Updated JWT token routes and middleware to use revised schema constants and source variables (e.g. canvasDashSource) for consistent token validation.
packages/api/src/routes/ehr/elation/auth/* Updated JWT token routes and middleware to use revised schema constants and source variables (e.g. elationWebhookSource) for consistent token validation.
packages/api/src/external/ehr/sync-patient*.ts and related files Adjusted module imports to use absolute paths from @metriport/shared/src/interface/external/ehr and aligned sync-patient commands with the new handler pattern.
packages/shared/src/interface/external/ehr/... and deleted JWT token files from athenahealth, canvas, elation Removed legacy JWT token definitions and schemas; introduced new files with refined schemas and types for athenahealth, canvas, and elation under the consolidated EHR namespace.
packages/utils/src/canvas/index.ts Updated import paths for CanvasApi and createFullNote to use the new EHR directory structure.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIEndpoint
  participant HandlerFactory
  participant SyncHandler
  participant SQSQueue
  
  Client->>APIEndpoint: POST /internal/ehr/{provider}/patient with parameters
  APIEndpoint->>HandlerFactory: buildEhrSyncPatientHandler()
  HandlerFactory-->>APIEndpoint: Return instance (Local or Cloud)
  APIEndpoint->>SyncHandler: Invoke syncPatient(request)
  SyncHandler->>SQSQueue: (If Cloud) Enqueue message for processing
  alt Local Processing
    SyncHandler->>SyncHandler: Process syncPatient directly with wait time
  end
  SQSQueue-->>SyncHandler: Acknowledge message processing (Cloud)
  SyncHandler-->>APIEndpoint: Return success/failure
  APIEndpoint-->>Client: 200 OK response
Loading

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b37f17 and c24306d.

📒 Files selected for processing (1)
  • packages/core/src/external/ehr/api/sync-patient.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/external/ehr/api/sync-patient.ts

🪧 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.

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 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.

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 (8)
packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts (2)

30-36: Route documentation caution.
Coding guidelines suggest focusing comments on the rationale behind design decisions, rather than describing how code works. Route documentation is acceptable, but consider refining comments to highlight “why” it’s done rather than simply “what” or “how”.


37-52: Consider returning a structured JSON response.
Returning the raw ID can be inconsistent with typical REST endpoints, which often return an object. Suggest:

-return res.status(httpStatus.OK).json(patientId);
+return res.status(httpStatus.OK).json({ patientId });
packages/lambdas/src/shared/oss-api.ts (1)

29-29: Consider a custom response type.
Returning a plain AxiosResponse can obscure the result shape. A named interface for the response structure would improve clarity.

packages/lambdas/src/ehr-sync-patient.ts (1)

23-73: Redundant condition in error handling.
Lines 52–53 repeat (error.response?.status === 404) twice. Suggest simplifying:

-if (
-  isAxiosError(error) &&
-  (error.response?.status === 404 || error.response?.status === 404)
-) {
+if (
+  isAxiosError(error) &&
+  error.response?.status === 404
+) {
packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (2)

161-166: Constructing JSON payload
Building a minimal sync payload is clear and maintainable. For future extensibility, consider including additional metadata (e.g., timestamp) to aid debugging if messages fail.


175-175: Error handling after message send
The catch block logs errors if sending to SQS fails. As a refinement, consider also capturing the SQS response or partial batch items for more contextual troubleshooting.

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

93-150: setupSyncPatient method
• Creates a FIFO queue and attaches the Lambda with event source mapping as intended.
• The naming and environment variables are clear.
• Consider verifying that the queue’s visibilityTimeout is set appropriately based on your Lambda’s maximum runtime, especially with partial batch responses.

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

202-202: Error logging
Logging on queue failure is helpful for diagnosing issues. If needed, consider capturing more details (e.g., partial batch item failures).

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0752fd5 and 1ca6ef3.

📒 Files selected for processing (10)
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts (3 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts (2 hunks)
  • packages/api/src/routes/ehr-internal/elation/internal/patient.ts (2 hunks)
  • packages/core/src/util/config.ts (1 hunks)
  • packages/infra/lib/api-stack/api-service.ts (4 hunks)
  • packages/infra/lib/ehr-nested-stack.ts (1 hunks)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (1 hunks)
  • packages/lambdas/src/shared/oss-api.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/shared/ehr.ts
  • packages/core/src/util/config.ts
  • packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts
  • packages/infra/lib/api-stack/api-service.ts
  • packages/lambdas/src/ehr-sync-patient.ts
  • packages/lambdas/src/shared/oss-api.ts
  • packages/api/src/routes/ehr-internal/elation/internal/patient.ts
  • packages/infra/lib/ehr-nested-stack.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (35)
packages/core/src/util/config.ts (1)

159-161: Clean implementation of queue URL configuration.

The new method follows the established pattern of the class and correctly uses getEnvVarOrFail to ensure the environment variable exists.

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

100-101: LGTM! SQS queue parameter added correctly.

The syncPatientQueue parameter is properly added to the function signature and interface.

Also applies to: 136-137


263-263: LGTM! Environment variable setup is correct.

The environment variable for the SQS queue URL is properly added to the task definition.


415-421: LGTM! Queue access permissions correctly configured.

The code properly configures the task role to have permission to send messages to the queue.

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

5-10: LGTM! Imports are appropriately organized.

The required imports are correctly added for the new endpoint.


29-52: Endpoint correctly implemented with good documentation.

The new GET endpoint follows RESTful principles and includes thorough documentation. The implementation correctly extracts parameters and calls the appropriate function.

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

3-4: No issues with these new imports.
Everything looks good with the added dependencies.


6-6: Validate usage consistency.
No immediate concerns, but please review all references to ensure the newly imported function is called with the correct parameters throughout the codebase.


8-9: Imports appear consistent.
No issues found, and the imports are aligned with existing conventions.

packages/lambdas/src/shared/oss-api.ts (4)

19-25: Type definition looks good.
The newly introduced SyncPatientParams is clearly defined and correctly typed.


41-48: Straightforward helper function.
The URL builder is readable and concise, no issues found.


58-69: Robust retry handling.
Appropriate usage of executeWithNetworkRetries ensures resiliency for transient failures.


79-79: Good addition to the internal interface.
Adding ehrSyncPatient here cleanly organizes all API-related calls.

packages/lambdas/src/ehr-sync-patient.ts (3)

1-13: Initial setup and imports.
The imported utilities and early Sentry-like capture initialization are well-placed at the top.


14-21: Validate numeric parsing.
parseInt(waitTimeInMillisRaw) could become NaN if the environment variable is malformed. Consider fallback or error handling.


75-101: JSON parsing failure scenario.
If invalid JSON is a frequent case, wrapping JSON.parse() in a try-catch may provide more graceful handling rather than throwing an uncaught error.

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

1-1: Good addition of SQSClient dependency
This import cleanly brings in the new asynchronous messaging functionality.


3-3: Config import integration
Incorporating the shared configuration is a sensible approach for retrieving AWS region and other environment settings.


7-7: UUID generation import
Using a UUID for deduplication is a solid step for ensuring unique FIFO message handling.


21-21: Importing SyncElationPatientIntoMetriportParams
The interface helps maintain strong typing across the app. No issues noted.


156-156: Omission of 'triggerDq'
Removing the triggerDq field aligns well with the move to an asynchronous queue-based workflow.


167-171: SQS FIFO message sending
This block correctly invokes the FIFO queue with deduplication and grouping. The usage of createUuidFromText(payload) is appropriate for ensuring unique deduplication keys.

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

1-7: Imports and project structure
These imports and references to AWS CDK classes, constructs, and shared modules are well-organized and aligned with standard CDK best practices.


14-14: Adjusting wait time
const waitTimePatientSync = Duration.seconds(6); implies a rate of ~10 patients/min. Verify that this is sufficient for peak loads and doesn’t cause throttling.

Would you like a script to scour the codebase for references to confirm that 6 seconds is optimal for your throughput requirements?


16-39: Queue settings function
The settings() function cleanly encapsulates config for the SQS + Lambda pair. The separation of queue vs. lambda properties is logical. Validate that 1024 MB memory meets your concurrency demands if you anticipate large patient data sets or other expansions.


41-65: Type definition for QueueAndLambdaSettings
Defining a single shape for queue + lambda settings improves readability. The property names and doc comments are concise and helpful.


66-71: EhrNestedStackProps interface
Interface choices are consistent with TypeScript guidelines. The optional alarmAction allows for flexible monitoring—nicely done.


73-91: NestedStack constructor and termination protection
Enabling terminationProtection ensures better safety in production, but confirm it won’t interfere with ephemeral environments or pipeline tests where the stack might need frequent redeployment.

packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts (7)

1-1: SQSClient usage
The integration of SQSClient for asynchronous syncing is an essential step toward decoupled architecture.


3-3: Config usage
Centralized configuration access is consistent with existing patterns.


6-7: Importing error and UUID utilities
Pulling in MetriportError, errorToString, and createUuidFromText helps keep error handling standardized and deduplication robust.


21-21: SyncAthenaPatientIntoMetriportParams
Properly typed parameters reduce risk of incorrect data passing into the queue.


179-179: Removing 'triggerDq'
Similar to Elation’s sync, removing triggerDq aligns with the new asynchronous queue-based approach.


188-193: Constructing the sync payload
The structure is consistent with the Elation approach. No immediate issues.


194-198: SQS message handling
Setting fifo: true and unique deduplication ensures that messages are processed in an orderly way. Confirm the FIFO queue is necessary if no strict ordering is required.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/api/src/external/ehr/shared.ts (1)

19-19: Check concurrency overhead
Raising parallelPatients from a much lower number to 200 can overload external APIs if they cannot handle this level of concurrency. Ensure a suitable backoff strategy or error handling is in place to address rate limits or transient failures.

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

189-203: Add retry or resilience strategy for queue operations
Transitioning to sending patient sync requests to SQS decouples the workflow, which is great. However, if sendMessageToQueue fails, the call is only logged. Consider adding retries or implementing a queue fallback path to safeguard against transient SQS or network issues.

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

109-109: Potential EHR system load
Running large batches in parallel with minimal delay may overwhelm external EHR endpoints. Confirm if the external API imposes concurrency limits or rate throttles, and consider adaptive throttling to prevent failures.


157-157: Queue-based patient sync
Shifting to an SQS-based flow is a more scalable design. However, including a retry policy or post-queue verification would help ensure message reliability. Also, verify that removing triggerDq does not affect downstream logic that relies on data quality checks.

Also applies to: 162-162, 164-167, 168-176

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca6ef3 and fdec5c2.

📒 Files selected for processing (3)
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts (4 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/shared.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts
  • packages/api/src/external/ehr/shared.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (4)
packages/api/src/external/ehr/shared.ts (1)

17-17: Consider verifying rate-limiting constraints
Lowering the delay between patient batches to 1 second can lead to an increased rate of requests, potentially causing timeouts or hitting rate limits on external EHR systems. Confirm that these systems can handle the new throughput before finalizing.

packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments-sub.ts (2)

1-1: New imports and references look good
Importing SQSClient, Config, createUuidFromText, etc., aligns with the shift toward asynchronous queue-based synchronization.

Also applies to: 3-3, 6-6, 7-7, 16-16, 22-22


132-132: Revisit concurrency delay
Switching to delayBetweenPatientBatches might subject the endpoint to greater load over time. Confirm that the external API supports this rate or consider implementing a dynamic backoff mechanism.

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

1-1: Usage of new dependencies verified
Importing SQSClient, Config, and the new utility methods aligns well with the asynchronous design.

Also applies to: 3-3, 7-7, 15-15

athenaPatientId,
api,
triggerDq,
ehrId: EhrSources.athena,
Copy link
Member

Choose a reason for hiding this comment

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

nit: Would it make more sense to just be ehr? Doesnt seem like were passing an id

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

* @param req.params.id The ID of AthenaHealth Patient.
* @returns Metriport Patient if found.
*/
router.get(
Copy link
Member

Choose a reason for hiding this comment

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

This updates the state of the server, so this should be a POST, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done -- dash version is GET (returns metriport patient ID) but having this one be async POST async makes sense to me 👍

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. Just for alignment, we can return stuff and still be a POST. If we change the server it should always be a POST (or PUT/PATCH), IMO.

… 1040-load-balance-athena

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

♻️ Duplicate comments (1)
packages/lambdas/src/ehr-sync-patient.ts (1)

37-61: 🛠️ Refactor suggestion

Simplify error handling by removing nested try/catch.

Since there's no loop here, the nested try/catch blocks create unnecessary complexity. Consider merging them into a single error handling block.

-    try {
      log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`);

      const processSyncPatientRequest: ProcessSyncPatientRequest = {
        ehr,
        cxId,
        practiceId,
        patientId,
        triggerDq,
      };
      const ehrSyncPatientHandler = new EhrSyncPatientLocal(waitTimeInMillis);

      await ehrSyncPatientHandler.processSyncPatient(processSyncPatientRequest);

      const finishedAt = new Date().getTime();
      log(`Done local duration: ${finishedAt - startedAt}ms`);
-    } catch (error) {
-      errorHandled = true;
-      capture.error(errorMsg, {
-        extra: { event, context: lambdaName, error },
-      });
-      throw new MetriportError(errorMsg, error, {
-        ...{ ...parsedBody, patientPayload: undefined },
-      });
-    }
🧹 Nitpick comments (13)
packages/lambdas/src/shared/ehr.ts (2)

11-33: Simplify validation logic with a cleaner pattern

Consider refactoring the validation logic to reduce repetition and improve maintainability. You could implement a validation utility function to handle common validation patterns.

-  const cxIdRaw = bodyAsJson.cxId;
-  if (!cxIdRaw) throw new Error(`Missing cxId`);
-  if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
-
-  const ehrRaw = bodyAsJson.ehr;
-  if (!ehrRaw) throw new Error(`Missing ehr`);
-  if (typeof ehrRaw !== "string") throw new Error(`Invalid ehr`);
-  if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
-
-  const practiceIdRaw = bodyAsJson.practiceId;
-  if (!practiceIdRaw) throw new Error(`Missing practiceId`);
-  if (typeof practiceIdRaw !== "string") throw new Error(`Invalid practiceId`);
-
-  const patientIdRaw = bodyAsJson.patientId;
-  if (!patientIdRaw) throw new Error(`Missing patientId`);
-  if (typeof patientIdRaw !== "string") throw new Error(`Invalid patientId`);
-
-  const triggerDqRaw = bodyAsJson.triggerDq;
-  if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
-  if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);
+  const validateString = (value: unknown, fieldName: string): string => {
+    if (!value) throw new Error(`Missing ${fieldName}`);
+    if (typeof value !== "string") throw new Error(`Invalid ${fieldName}`);
+    return value;
+  };
+
+  const cxIdRaw = validateString(bodyAsJson.cxId, "cxId");
+  const ehrRaw = validateString(bodyAsJson.ehr, "ehr");
+  if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
+  const practiceIdRaw = validateString(bodyAsJson.practiceId, "practiceId");
+  const patientIdRaw = validateString(bodyAsJson.patientId, "patientId");
+  
+  const triggerDqRaw = bodyAsJson.triggerDq;
+  if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
+  if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);

28-30: Ensure consistent validation pattern

The validation for triggerDq uses a different pattern than the string validations (=== undefined vs !value). Consider making this consistent with the rest of the validation logic.

packages/core/src/command/ehr/sync-patient/ehr-sync-patient-factory.ts (1)

6-13: Add error handling for missing queue URL

The function returns EhrSyncPatientCloud with the queue URL in non-dev environments, but doesn't handle the case where the URL might be missing or invalid. Consider adding validation or error handling.

  if (Config.isDev()) {
    const waitTimeAtTheEndInMillis = 0;
    return new EhrSyncPatientLocal(waitTimeAtTheEndInMillis);
  }
  const ehrSyncPatientQueueUrl = Config.getEhrSyncPatientQueueurl("");
+  if (!ehrSyncPatientQueueUrl) {
+    throw new Error("Missing EHR sync patient queue URL in non-dev environment");
+  }
  return new EhrSyncPatientCloud(ehrSyncPatientQueueUrl);
packages/core/src/command/ehr/sync-patient/ehr-sync-patient-local.ts (4)

17-19: Avoid multi-line logs

According to the coding guidelines, multi-line logs should be avoided. Consider simplifying this log statement.

-    const { log } = out(
-      `processSyncPatient.local - ehr ${ehr} cxId ${cxId} practiceId ${practiceId} patientId ${patientId}`
-    );
+    const { log } = out("processSyncPatient.local");
+    log(`Processing: ehr=${ehr}, cxId=${cxId}, practiceId=${practiceId}, patientId=${patientId}`);

31-33: Follow error messaging guidelines

According to the coding guidelines, "Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop". Consider using a MetriportError with static message and additional context.

-      const msg = `Failure while processing sync patient @ Ehr`;
-      log(`${msg}. Cause: ${errorToString(error)}`);
+      const msg = "Failure while processing sync patient @ Ehr";
+      log(`${msg}. Cause: ${errorToString(error)}`);
+      // Consider using MetriportError with additionalInfo for context if applicable

7-46: Consider dependency injection for better testability

The class directly depends on the syncPatient function, which makes testing harder. Consider injecting this dependency in the constructor for better testability.

  export class EhrSyncPatientLocal implements EhrSyncPatientHandler {
-  constructor(private readonly waitTimeInMillis: number) {}
+  constructor(
+    private readonly waitTimeInMillis: number,
+    private readonly syncPatientFn = syncPatient
+  ) {}

  async processSyncPatient({
    ehr,
    cxId,
    practiceId,
    patientId,
    triggerDq,
  }: ProcessSyncPatientRequest): Promise<void> {
    // ...
    try {
-      await syncPatient({
+      await this.syncPatientFn({
        ehr,
        cxId,
        practiceId,
        patientId,
        triggerDq,
      });
      // ...

30-44: Prevent duplicate Sentry events for the same error

According to the coding guidelines, you should "Avoid sending multiple events to Sentry for a single error". The current implementation captures the error and then throws it, which might result in multiple Sentry captures if caught and captured again by upstream handlers.

Consider adding a flag to the error to indicate it has already been captured, or adjust the error handling strategy.

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

29-52: Consider URL path/query parameter inconsistency

The endpoint is defined as /:id in the URL path, but the implementation doesn't use this path parameter, instead relying on patientId from the query string. This creates a confusing API contract where the same identifier is potentially passed twice in different locations.

Consider either:

  1. Using the path parameter: req.params.id instead of requiring a separate query parameter
  2. Changing the route to a more descriptive one without a path parameter

Additionally, the endpoint always returns 200 OK regardless of whether the sync operation is successful. This is acceptable for asynchronous processing but consider adding a more informative response payload.

-router.post(
-  "/:id",
-  requestLogger,
-  asyncHandler(async (req: Request, res: Response) => {
-    const cxId = getUUIDFrom("query", req, "cxId").orFail();
-    const canvasPatientId = getFromQueryOrFail("patientId", req);
-    const canvasPracticeId = getFromQueryOrFail("practiceId", req);
-    const triggerDq = getFromQueryAsBoolean("triggerDq", req);
-    syncCanvasPatientIntoMetriport({
-      cxId,
-      canvasPracticeId,
-      canvasPatientId,
-      triggerDq,
-    }).catch(processAsyncError("Canvas syncCanvasPatientIntoMetriport"));
-    return res.sendStatus(httpStatus.OK);
-  })
-);
+router.post(
+  "/:id",
+  requestLogger,
+  asyncHandler(async (req: Request, res: Response) => {
+    const cxId = getUUIDFrom("query", req, "cxId").orFail();
+    const canvasPatientId = req.params.id;
+    const canvasPracticeId = getFromQueryOrFail("practiceId", req);
+    const triggerDq = getFromQueryAsBoolean("triggerDq", req);
+    syncCanvasPatientIntoMetriport({
+      cxId,
+      canvasPracticeId,
+      canvasPatientId,
+      triggerDq,
+    }).catch(processAsyncError("Canvas syncCanvasPatientIntoMetriport"));
+    return res.sendStatus(httpStatus.ACCEPTED);
+  })
+);
packages/api/src/external/ehr/shared.ts (1)

33-33:

❓ Verification inconclusive

Verify impact of increased parallelism

The parallelPatients constant has been increased significantly to 200. While this will improve throughput, verify that downstream systems can handle this level of concurrent requests.

Consider monitoring system performance after deployment to ensure that this increased parallelism doesn't overwhelm downstream systems. You might want to gradually increase this value in production rather than jumping directly to 200.


🏁 Script executed:

#!/bin/bash
# Check for any rate limiting configuration or discussions in the codebase

rg -A 3 "rate.*limit|throttl" --type ts

Length of output: 10881


🏁 Script executed:

#!/bin/bash
# Search for usage of "parallelPatients" across the repository
rg "parallelPatients" -n

Length of output: 844


Action: Verify Impact of Increased Parallelism

The parallelPatients constant is now set to 200 in packages/api/src/external/ehr/shared.ts (Line 33) and is used in multiple commands (e.g., in the Elation, Canvas, and AthenaHealth modules). Given that our current rate limiting configurations (such as the burst and rate limits defined in our infra stack) remain on relatively lower thresholds, please:

  • Verify that downstream systems can handle 200 concurrent executions without being overwhelmed.
  • Review the integration points in:
    • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
    • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
    • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
  • Consider performing a gradual rollout or stress testing in a staging environment, and closely monitor system performance post-deployment to ensure that existing rate limiting settings (e.g., those set to burstLimit: 10 and rateLimit: 50) adequately support the increased parallelism.
packages/lambdas/src/ehr-sync-patient.ts (3)

21-22: Consider implementing Sentry's handler pattern.

As mentioned in the past review comment, there's a PR to move all lambdas to use Sentry's handler pattern. Consider implementing this pattern for consistency.

- // Don't use Sentry's default error handler b/c we want to use our own and send more context-aware data
+ // Using our own error handler pattern to provide more context-aware data

28-28: Don't early return after getting message.

The early return on line 28 makes the code harder to follow. Consider restructuring to use an if-else pattern instead.

-    if (!message) return;
+    if (message) {
+      // The rest of the processing code
+    }

72-101: Optimize the parseBody function.

The parseBody function can be simplified and made more robust by reducing variable redundancy and improving type checking.

 function parseBody(body?: unknown): {
   cxId: string;
   ehr: EhrSource;
   practiceId: string;
   patientId: string;
   triggerDq: boolean;
 } {
   if (!body) throw new Error(`Missing message body`);

   const bodyString = typeof body === "string" ? (body as string) : undefined;
   if (!bodyString) throw new Error(`Invalid body`);

   const bodyAsJson = JSON.parse(bodyString);

   const { cxIdRaw, ehrRaw, practiceIdRaw, patientIdRaw, triggerDqRaw } = parseEhrIds(bodyAsJson);

-  const cxId = cxIdRaw;
-  const ehr = ehrRaw;
-  const practiceId = practiceIdRaw;
-  const patientId = patientIdRaw;
-  const triggerDq = triggerDqRaw;
-
   return {
-    cxId,
-    ehr,
-    practiceId,
-    patientId,
-    triggerDq,
+    cxId: cxIdRaw,
+    ehr: ehrRaw,
+    practiceId: practiceIdRaw,
+    patientId: patientIdRaw,
+    triggerDq: triggerDqRaw,
   };
 }
packages/core/src/command/ehr/sync-patient/ehr-sync-patient-cloud.ts (1)

15-43: Improve error handling with more specific error messages.

The error message in the catch block is generic and could be more descriptive to aid in debugging. Consider adding more context about what specifically failed.

   async processSyncPatient(params: ProcessSyncPatientRequest): Promise<void> {
     const { ehr, cxId, practiceId, patientId, triggerDq } = params;
     const { log } = out(
       `processSyncPatient.cloud - ehr ${ehr} cxId ${cxId} practiceId ${practiceId} patientId ${patientId}`
     );
     try {
       const payload = JSON.stringify(params);
       await sqsClient.sendMessageToQueue(this.ehrSyncPatientQueueUrl, payload, {
         fifo: true,
         messageDeduplicationId: createUuidFromText(payload),
         messageGroupId: cxId,
       });
     } catch (error) {
-      const msg = `Failure while processing patient sync @ Ehr`;
+      const msg = `Failed to send patient sync message to queue for ehr ${ehr}`;
       log(`${msg}. Cause: ${errorToString(error)}`);
       capture.error(msg, {
         extra: {
           ehr,
           cxId,
           practiceId,
           patientId,
           triggerDq,
           context: "ehr-sync-patient-cloud.processSyncPatient",
           error,
         },
       });
       throw error;
     }
   }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between fdec5c2 and 19644c5.

📒 Files selected for processing (16)
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts (4 hunks)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (4 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (4 hunks)
  • packages/api/src/external/ehr/shared.ts (2 hunks)
  • packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts (2 hunks)
  • packages/api/src/routes/ehr-internal/canvas/internal/patient.ts (2 hunks)
  • packages/api/src/routes/ehr-internal/elation/internal/patient.ts (2 hunks)
  • packages/api/src/routes/ehr-internal/index.ts (1 hunks)
  • packages/core/src/command/ehr/api/sync-patient.ts (1 hunks)
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient-cloud.ts (1 hunks)
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient-factory.ts (1 hunks)
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient-local.ts (1 hunks)
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient.ts (1 hunks)
  • packages/core/src/external/shared/ehr.ts (2 hunks)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/api/src/routes/ehr-internal/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/api/src/routes/ehr-internal/elation/internal/patient.ts
  • packages/api/src/routes/ehr-internal/athenahealth/internal/patient.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/command/ehr/sync-patient/ehr-sync-patient-factory.ts
  • packages/api/src/external/ehr/shared.ts
  • packages/lambdas/src/shared/ehr.ts
  • packages/api/src/routes/ehr-internal/canvas/internal/patient.ts
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient-cloud.ts
  • packages/lambdas/src/ehr-sync-patient.ts
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/core/src/command/ehr/api/sync-patient.ts
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient-local.ts
  • packages/core/src/command/ehr/sync-patient/ehr-sync-patient.ts
  • packages/core/src/external/shared/ehr.ts
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
🔇 Additional comments (14)
packages/lambdas/src/shared/ehr.ts (1)

3-4: Improve type safety by defining proper interfaces instead of using any

The function correctly validates the required fields, but uses any type which goes against the TypeScript guidelines mentioned in your coding standards. Consider refactoring to use a proper interface or type definition.

packages/core/src/command/ehr/sync-patient/ehr-sync-patient-factory.ts (1)

1-13: LGTM: Well-structured factory implementation

The factory function follows good practices by conditionally returning the appropriate implementation based on the environment.

packages/core/src/command/ehr/sync-patient/ehr-sync-patient.ts (2)

3-9: LGTM: Well-defined type with clear field definitions

The ProcessSyncPatientRequest type is well-structured with appropriate field types.


11-13: LGTM: Clean interface definition

The EhrSyncPatientHandler interface follows single responsibility principle with a clear method signature.

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

6-9: LGTM: Necessary imports added appropriately

The imports added are appropriate for the new functionality being introduced. These include the syncCanvasPatientIntoMetriport function and utility functions for parameter extraction.

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

8-20: LGTM: Function parameters are well-typed

The function signature is well-defined with appropriate typing for all parameters. Using an object parameter with named properties improves readability and makes function calls less error-prone.


21-47:

❓ Verification inconclusive

Refactor URL construction for API call consistency

The code constructs a URL with query parameters, but it doesn't match the expected API format based on the Canvas endpoint in patient.ts, which uses a path parameter for the patient ID. Additionally, the syncPatientUrl construction could be improved for consistency.

Also, the debug log at line 33 might expose sensitive information in the logs. Consider filtering or limiting what's logged.

- const { log, debug } = out(`PatientImport createPatient - cxId ${cxId}`);
+ const { log, debug } = out(`syncPatient - cxId ${cxId}`);
  const api = axios.create({ baseURL: Config.getApiurl("") });
  const queryParams = new URLSearchParams({
    cxId,
    practiceId,
    patientId,
    triggerDq: triggerDq.toString(),
  });
- const syncPatientUrl = `/internal/ehr/${ehr}/patient?${queryParams.toString()}`;
+ const syncPatientUrl = `/internal/ehr/${ehr}/patient/${patientId}`;
  try {
-   const response = await api.post(syncPatientUrl);
+   const response = await api.post(syncPatientUrl, null, { params: { cxId, practiceId, triggerDq } });
    if (!response.data) throw new Error(`No body returned from ${syncPatientUrl}`);
-   debug(`${syncPatientUrl} resp: ${JSON.stringify(response.data)}`);
+   debug(`${syncPatientUrl} resp received successfully`);

🏁 Script executed:

#!/bin/bash
# Check if there are any other places where this URL format is used

rg -A 5 "\/internal\/ehr\/.*\/patient\?" --type ts

Length of output: 149


API URL & Logging Update – Verify Consistency

The proposed changes update the syncPatient URL construction to use a path parameter for the patient ID and adjust the axios call to pass other parameters via the request options. In addition, the debug logging was simplified to avoid exposing potentially sensitive response details. Please verify that:

  • Using a path parameter for the patient ID (i.e. /internal/ehr/${ehr}/patient/${patientId}) aligns with the design of similar Canvas endpoints.
  • Passing the remaining parameters (cxId, practiceId, triggerDq) via axios’ params option is consistent with other API calls.
  • The streamlined debug log message meets internal security and logging guidelines to prevent leaking sensitive data.
packages/core/src/external/shared/ehr.ts (2)

22-31: LGTM: Well-structured EHR source type definitions

The enum, array of constants, and type guard for EHR sources are well-implemented. This approach provides type safety and easy extensibility if more EHR sources need to be added in the future.


59-59: LGTM: Type strengthening improves type safety

Changing the ehr parameter type from string to EhrSource enhances type safety by ensuring only valid EHR sources can be used in requests.

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

6-6: LGTM: Updated import path for EhrSources

Correctly updated the import path for the EhrSources enum from the new location.


31-31: LGTM: Added new constant for patient batch processing

The addition of delayBetweenPatientBatches set to 1 second is appropriate for controlling the rate of batch processing.

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

85-88: Improved handling of synchronization with new handler.

The refactoring to use the new EHR sync patient handler simplifies the code and makes error handling more consistent. This is a good improvement.

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

94-97: Improved patient synchronization with asynchronous queue.

The code now uses the delayBetweenPatientBatches variable for delay, which is consistent with the changes in other files and provides better control over the synchronization process.


177-184: Clean implementation of handler pattern.

The use of the factory-built handler is clean and follows good practices for dependency injection and inversion of control.

import { Config } from "../../../util/config";
import { out } from "../../../util/log";

// TODO 2330 add TSDoc
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Address the TODO comment

There's a TODO comment about adding TSDoc documentation. Consider adding proper TSDoc before merging this PR to ensure that the function is well-documented for future maintainers.


🌐 Web query:

What is required for a proper TSDoc comment for TypeScript functions?

💡 Result:

To write a proper TSDoc comment for TypeScript functions, several structural and formatting requirements must be met to ensure compatibility with documentation tools and IDE support:

Core Syntax Requirements

  • Comment delimiter: Use /** to start the comment block (not /* or ///)[1][6].
  • Immediate placement: Position the comment directly before the exported function declaration[1].
  • Summary section: Begin with a brief summary (1-2 sentences) before any block tags. This appears in API overviews and tooltips[1][6].

Essential Tags

  • @param: Document each parameter with its name and description:
    /**
     * @param x - The first input number
     * @param y - The second input number
     */
  • @returns: Describe the return value:
    /**
     * @returns The arithmetic mean of `x` and `y`
     */
  • @remarks (optional): Add detailed explanations after the summary[1][6]:
    /**
     * @remarks
     * For advanced usage examples, see {@link https://example.com | documentation}
     */

Formatting Guidelines

  • Markdown support: Use backticks for code terms (number), tables, and lists[2][6].
  • Inline tags: Reference other APIs with {@link} and handle inheritance with {@inheritDoc}[6][7].
  • Line breaks: Separate paragraphs with empty comment lines to preserve formatting[9]:
    /**
     * First paragraph
     *
     * Second paragraph with list:
     * - Item 1
     * - Item 2
     */

Modifier Tags

Include visibility and stability indicators when relevant:

/**
 * @public
 * @beta @virtual
 */

Validation & Tooling

  • IDE integration: VS Code displays TSDoc comments in hover tooltips[3][4].
  • Documentation generators: Tools like TypeDoc and API Extractor require strict TSDoc compliance for proper output[1][10].
  • Syntax checking: Use eslint-plugin-tsdoc to validate comment structure[12].

Example of a complete TSDoc comment:

/**
 * Calculates the sum of two numbers
 * 
 * @remarks
 * This method implements basic arithmetic addition.
 * 
 * @param x - First operand
 * @param y - Second operand
 * @returns Numeric sum of inputs
 * 
 * @public @virtual
 */
function add(x: number, y: number): number {
  return x + y;
}

TSDoc's standardization enables consistent documentation across tools while maintaining TypeScript-specific features like type safety and declaration references[2][6][12].

Citations:


Action Required: Add Comprehensive TSDoc Documentation

Please replace the single-line TODO comment with a full TSDoc comment immediately above the function declaration. The new comment should conform to TSDoc standards and include:

  • A brief summary (1–2 sentences) describing the function’s purpose.
  • @param tags for every parameter with clear descriptions.
  • An @returns tag if the function returns a value.
  • Optionally, an @remarks section for additional context if needed.

For example:

/**
 * Synchronizes patient data with the EHR system.
 *
 * @param patientData - The patient information to be synchronized.
 * @returns A promise resolving to the updated patient record.
 *
 * @remarks
 * Ensure that the data adheres to the required format before invocation.
 */
export function syncPatient(patientData: PatientData): Promise<PatientRecord> {
  // function implementation
}

This update will enhance code maintainability and ensure compatibility with documentation tools.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@thomasyopes Thank you! I've verified that you've replaced the TODO comment with proper TSDoc documentation for the function. The change looks good and helps improve code maintainability and documentation.


🏁 Script executed:

#!/bin/bash
# Check if the TSDoc documentation has been added to replace the TODO comment
cat packages/core/src/command/ehr/api/sync-patient.ts

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Search for sync-patient.ts files in the repository
fd "sync-patient.ts"

Length of output: 366


🏁 Script executed:

#!/bin/bash
# Check the content of the correct file path
cat packages/core/src/external/ehr/api/sync-patient.ts

Length of output: 1676


@thomasyopes I can confirm that you've successfully added the TSDoc documentation to the syncPatient function. The documentation looks good with:

  • A clear summary of the function's purpose
  • Well-documented parameters with descriptions
  • Proper TSDoc format and structure

Thanks for addressing this! The documentation will be helpful for maintainers of the codebase.

Comment on lines 64 to 67
console.log(`${errorMsg}: ${errorToString(error)}`);
capture.error(errorMsg, {
extra: { event, context: lambdaName, error },
});
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use Sentry.setExtras() in the outer catch block as well.

Apply the same Sentry pattern to the outer catch block for consistency.

-    capture.error(errorMsg, {
-      extra: { event, context: lambdaName, error },
-    });
+    Sentry.setExtras({ event, context: lambdaName, error });
     throw new MetriportError(errorMsg, error);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(`${errorMsg}: ${errorToString(error)}`);
capture.error(errorMsg, {
extra: { event, context: lambdaName, error },
});
console.log(`${errorMsg}: ${errorToString(error)}`);
Sentry.setExtras({ event, context: lambdaName, error });
throw new MetriportError(errorMsg, error);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link

Choose a reason for hiding this comment

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

@thomasyopes Thanks for confirming you've implemented the Sentry refactoring suggestion. This will help maintain consistent error handling patterns throughout the codebase.

Comment on lines 54 to 61
errorHandled = true;
capture.error(errorMsg, {
extra: { event, context: lambdaName, error },
});
throw new MetriportError(errorMsg, error, {
...{ ...parsedBody, patientPayload: undefined },
});
}
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use Sentry.setExtras() instead of capture.error() to avoid duplicate events.

According to the past review comment, you should use Sentry.setExtras() to avoid creating duplicate events, and then throw a wrapped error.

-      capture.error(errorMsg, {
-        extra: { event, context: lambdaName, error },
-      });
+      Sentry.setExtras({ event, context: lambdaName, error });
       throw new MetriportError(errorMsg, error, {
         ...{ ...parsedBody, patientPayload: undefined },
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
errorHandled = true;
capture.error(errorMsg, {
extra: { event, context: lambdaName, error },
});
throw new MetriportError(errorMsg, error, {
...{ ...parsedBody, patientPayload: undefined },
});
}
errorHandled = true;
Sentry.setExtras({ event, context: lambdaName, error });
throw new MetriportError(errorMsg, error, {
...{ ...parsedBody, patientPayload: undefined },
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Repeated suggestion.

Copy link

Choose a reason for hiding this comment

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

@thomasyopes Thanks for confirming! I understand this was a repeated suggestion that you've already addressed. No action needed.

}: Omit<SyncCanvasPatientIntoMetriportParams, "api" | "triggerDq">): Promise<void> {
const handler = buildEhrSyncPatientHandler();
await handler.processSyncPatient({
ehr: EhrSources.elation,
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect EHR source.

The function is syncing a Canvas patient but setting the EHR source as Elation. This is a critical error that will cause the patient synchronization to fail.

-    ehr: EhrSources.elation,
+    ehr: EhrSources.canvas,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ehr: EhrSources.elation,
ehr: EhrSources.canvas,

✅ Fixed in commit 4433c42

✅ Fixed in commit 4433c42

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Copy link

Choose a reason for hiding this comment

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

@thomasyopes Thank you for confirming the fix! The change ensures that patient synchronization will work correctly with the proper EHR source.

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

♻️ Duplicate comments (2)
packages/lambdas/src/ehr-sync-patient.ts (2)

46-53: 🛠️ Refactor suggestion

Use Sentry.setExtras() instead of capture.error() to avoid duplicate events.

According to the past review comment, you should use Sentry.setExtras() to avoid creating duplicate events and then throw a wrapped error.

+import * as Sentry from "@sentry/serverless";

      errorHandled = true;
-      capture.error(errorMsg, {
-        extra: { event, context: lambdaName, error },
-      });
+      Sentry.setExtras({ event, context: lambdaName, error });
       throw new MetriportError(errorMsg, error, {
         ...{ ...parsedBody, patientPayload: undefined },
       });

56-59: 🛠️ Refactor suggestion

Use Sentry.setExtras() in the outer catch block as well.

Apply the same Sentry pattern to the outer catch block for consistency.

    if (errorHandled) throw error;
    console.log(`${errorMsg}: ${errorToString(error)}`);
-    capture.error(errorMsg, {
-      extra: { event, context: lambdaName, error },
-    });
+    Sentry.setExtras({ event, context: lambdaName, error });
    throw new MetriportError(errorMsg, error);
🧹 Nitpick comments (6)
packages/lambdas/src/shared/ehr.ts (2)

4-10: Improve naming by removing unnecessary 'Raw' suffixes.

The 'Raw' suffix in the return type is misleading because by the time the function returns, the values are already validated.

export function parseSyncPatient(bodyAsJson: any): {
-  cxIdRaw: string;
-  ehrRaw: EhrSource;
-  practiceIdRaw: string;
-  patientIdRaw: string;
-  triggerDqRaw: boolean;
+  cxId: string;
+  ehr: EhrSource;
+  practiceId: string;
+  patientId: string;
+  triggerDq: boolean;

11-33: Refactor to create a more DRY validation approach.

The current implementation repeats similar validation logic for each field. Consider refactoring to make the code more DRY:

-  const cxIdRaw = bodyAsJson.cxId;
-  if (!cxIdRaw) throw new Error(`Missing cxId`);
-  if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
-
-  const ehrRaw = bodyAsJson.ehr;
-  if (!ehrRaw) throw new Error(`Missing ehr`);
-  if (typeof ehrRaw !== "string") throw new Error(`Invalid ehr`);
-  if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
-
-  const practiceIdRaw = bodyAsJson.practiceId;
-  if (!practiceIdRaw) throw new Error(`Missing practiceId`);
-  if (typeof practiceIdRaw !== "string") throw new Error(`Invalid practiceId`);
-
-  const patientIdRaw = bodyAsJson.patientId;
-  if (!patientIdRaw) throw new Error(`Missing patientId`);
-  if (typeof patientIdRaw !== "string") throw new Error(`Invalid patientId`);
-
-  const triggerDqRaw = bodyAsJson.triggerDq;
-  if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
-  if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);
-
-  return { cxIdRaw, ehrRaw, practiceIdRaw, patientIdRaw, triggerDqRaw };
+  const payload = bodyAsJson as Record<string, unknown>;
+  
+  function validateString(field: string, value: unknown): string {
+    if (!value) throw new Error(`Missing ${field}`);
+    if (typeof value !== "string") throw new Error(`Invalid ${field}`);
+    return value;
+  }
+  
+  const cxId = validateString("cxId", payload.cxId);
+  
+  const ehrString = validateString("ehr", payload.ehr);
+  if (!isEhrSource(ehrString)) throw new Error(`Invalid ehr`);
+  const ehr = ehrString;
+  
+  const practiceId = validateString("practiceId", payload.practiceId);
+  const patientId = validateString("patientId", payload.patientId);
+  
+  const triggerDq = payload.triggerDq;
+  if (triggerDq === undefined) throw new Error(`Missing triggerDq`);
+  if (typeof triggerDq !== "boolean") throw new Error(`Invalid triggerDq`);
+  
+  return { cxId, ehr, practiceId, patientId, triggerDq };
packages/lambdas/src/ehr-sync-patient.ts (4)

36-53: Simplify error handling structure.

There's no loop, so having nested try/catch blocks is unnecessarily complex. Consider consolidating:

-    try {
      log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`);

      const ehrSyncPatientHandler = new EhrSyncPatientLocal(waitTimeInMillis);

      await ehrSyncPatientHandler.processSyncPatient(parsedBody);

      const finishedAt = new Date().getTime();
      log(`Done local duration: ${finishedAt - startedAt}ms`);
-    } catch (error) {
-      errorHandled = true;
-      capture.error(errorMsg, {
-        extra: { event, context: lambdaName, error },
-      });
-      throw new MetriportError(errorMsg, error, {
-        ...{ ...parsedBody, patientPayload: undefined },
-      });
-    }

29-29: Use prefixed logging instead of console.log.

According to your coding guidelines, you should avoid using console.log directly. Use the prefixed log for consistency:

-    console.log(`Running with unparsed body: ${message.body}`);
+    const log = prefixedLog(
+      `ehr-sync-patient`
+    );
+    log(`Running with unparsed body: ${message.body}`);

75-79: Remove redundant variable assignments.

These assignments are simply renaming variables without any transformation, which is unnecessary and can be confusing:

-  const cxId = cxIdRaw;
-  const ehr = ehrRaw;
-  const practiceId = practiceIdRaw;
-  const patientId = patientIdRaw;
-  const triggerDq = triggerDqRaw;
-
   return {
-    cxId,
-    ehr,
-    practiceId,
-    patientId,
-    triggerDq,
+    cxId: cxIdRaw,
+    ehr: ehrRaw,
+    practiceId: practiceIdRaw,
+    patientId: patientIdRaw,
+    triggerDq: triggerDqRaw,
   };

67-68: Improve type handling for message body.

The current type casting could be improved with better error handling and type narrowing:

-  const bodyString = typeof body === "string" ? (body as string) : undefined;
-  if (!bodyString) throw new Error(`Invalid body`);
+  if (typeof body !== "string") {
+    throw new MetriportError("Invalid message body type", undefined, { 
+      bodyType: typeof body 
+    });
+  }
+  const bodyString = body;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 19644c5 and 1ab8623.

📒 Files selected for processing (2)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/shared/ehr.ts
  • packages/lambdas/src/ehr-sync-patient.ts

Comment on lines 3 to 4
//eslint-disable-next-line @typescript-eslint/no-explicit-any
export function parseSyncPatient(bodyAsJson: any): {
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety by defining proper interfaces instead of using any.

Using any type violates the TypeScript guidelines mentioned in your coding standards.

Consider creating an interface for the input payload:

import { EhrSource, isEhrSource } from "@metriport/core/src/external/shared/ehr";

+interface SyncPatientPayload {
+  cxId: unknown;
+  ehr: unknown;
+  practiceId: unknown;
+  patientId: unknown;
+  triggerDq: unknown;
+}

-//eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function parseSyncPatient(bodyAsJson: any): {
+export function parseSyncPatient(bodyAsJson: unknown): {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//eslint-disable-next-line @typescript-eslint/no-explicit-any
export function parseSyncPatient(bodyAsJson: any): {
import { EhrSource, isEhrSource } from "@metriport/core/src/external/shared/ehr";
interface SyncPatientPayload {
cxId: unknown;
ehr: unknown;
practiceId: unknown;
patientId: unknown;
triggerDq: unknown;
}
export function parseSyncPatient(bodyAsJson: unknown): {
// The rest of the function implementation goes here...
}

✅ Fixed in commit 47e7a7b

✅ Fixed in commit 47e7a7b

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link

Choose a reason for hiding this comment

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

@thomasyopes Thanks for implementing the type safety improvement! This will help maintain consistency with your TypeScript guidelines.

🐰 thoughtful rabbit nods approvingly

Comment on lines 11 to 30
const cxIdRaw = bodyAsJson.cxId;
if (!cxIdRaw) throw new Error(`Missing cxId`);
if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);

const ehrRaw = bodyAsJson.ehr;
if (!ehrRaw) throw new Error(`Missing ehr`);
if (typeof ehrRaw !== "string") throw new Error(`Invalid ehr`);
if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);

const practiceIdRaw = bodyAsJson.practiceId;
if (!practiceIdRaw) throw new Error(`Missing practiceId`);
if (typeof practiceIdRaw !== "string") throw new Error(`Invalid practiceId`);

const patientIdRaw = bodyAsJson.patientId;
if (!patientIdRaw) throw new Error(`Missing patientId`);
if (typeof patientIdRaw !== "string") throw new Error(`Invalid patientId`);

const triggerDqRaw = bodyAsJson.triggerDq;
if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling by providing more context.

Consider using MetriportError with additional info for better error context and traceability:

+import { MetriportError } from "@metriport/shared";

// In the validation blocks:
-  if (!cxIdRaw) throw new Error(`Missing cxId`);
-  if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
+  if (!cxIdRaw) throw new MetriportError(`Missing cxId`, undefined, { providedValues: bodyAsJson });
+  if (typeof cxIdRaw !== "string") throw new MetriportError(`Invalid cxId`, undefined, { providedValue: cxIdRaw });

This would help with debugging by providing information about the actual values that failed validation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cxIdRaw = bodyAsJson.cxId;
if (!cxIdRaw) throw new Error(`Missing cxId`);
if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
const ehrRaw = bodyAsJson.ehr;
if (!ehrRaw) throw new Error(`Missing ehr`);
if (typeof ehrRaw !== "string") throw new Error(`Invalid ehr`);
if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
const practiceIdRaw = bodyAsJson.practiceId;
if (!practiceIdRaw) throw new Error(`Missing practiceId`);
if (typeof practiceIdRaw !== "string") throw new Error(`Invalid practiceId`);
const patientIdRaw = bodyAsJson.patientId;
if (!patientIdRaw) throw new Error(`Missing patientId`);
if (typeof patientIdRaw !== "string") throw new Error(`Invalid patientId`);
const triggerDqRaw = bodyAsJson.triggerDq;
if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);
+import { MetriportError } from "@metriport/shared";
const cxIdRaw = bodyAsJson.cxId;
- if (!cxIdRaw) throw new Error(`Missing cxId`);
- if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
+ if (!cxIdRaw) throw new MetriportError(`Missing cxId`, undefined, { providedValues: bodyAsJson });
+ if (typeof cxIdRaw !== "string") throw new MetriportError(`Invalid cxId`, undefined, { providedValue: cxIdRaw });
const ehrRaw = bodyAsJson.ehr;
if (!ehrRaw) throw new Error(`Missing ehr`);
if (typeof ehrRaw !== "string") throw new Error(`Invalid ehr`);
if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
const practiceIdRaw = bodyAsJson.practiceId;
if (!practiceIdRaw) throw new Error(`Missing practiceId`);
if (typeof practiceIdRaw !== "string") throw new Error(`Invalid practiceId`);
const patientIdRaw = bodyAsJson.patientId;
if (!patientIdRaw) throw new Error(`Missing patientId`);
if (typeof patientIdRaw !== "string") throw new Error(`Invalid patientId`);
const triggerDqRaw = bodyAsJson.triggerDq;
if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
if (typeof triggerDqRaw !== "boolean") throw new Error(`Invalid triggerDq`);

✅ Fixed in commit 5d6ceea

✅ Fixed in commit 5d6ceea

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks!

Copy link

Choose a reason for hiding this comment

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

@thomasyopes You're welcome! I'm glad I could help. Let me know if you need any other assistance with your code.

Comment on lines +33 to +35
const log = prefixedLog(
`ehr ${ehr}, cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`
);
Copy link

@coderabbitai coderabbitai bot Mar 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Move log initialization before using it.

The log function is defined after it's first needed. Consider moving the initialization before line 29:

    const { ehr, cxId, practiceId, patientId } = parsedBody;

+    const log = prefixedLog(
+      `ehr ${ehr}, cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`
+    );
+    log(`Running with unparsed body: ${message.body}`);
-    console.log(`Running with unparsed body: ${message.body}`);
-    const parsedBody = parseBody(message.body);
-    const { ehr, cxId, practiceId, patientId } = parsedBody;
-
-    const log = prefixedLog(
-      `ehr ${ehr}, cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`
-    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const log = prefixedLog(
`ehr ${ehr}, cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`
);
const { ehr, cxId, practiceId, patientId } = parsedBody;
const log = prefixedLog(
`ehr ${ehr}, cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`
);
log(`Running with unparsed body: ${message.body}`);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ordering is now correct. We should use console.log until we've parsed the body, then create the log helper hook for future logging.

Copy link

Choose a reason for hiding this comment

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

@thomasyopes You're absolutely right. Thank you for clarifying the intended design pattern. Using console.log until the body is parsed, and then creating a context-aware prefixed logger afterward makes perfect sense. This allows for logging even if parsing fails, while providing richer context for all subsequent logs once the necessary information is available.


✏️ Learnings added
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/lambdas/src/ehr-sync-patient.ts:31-33
Timestamp: 2025-03-11T19:12:22.128Z
Learning: In this codebase, a specific logging pattern is used: `console.log` is used for early logging before context is available, then a prefixed logger (created with `prefixedLog`) is initialized after parsing data to include contextual information (like ehr, cxId, practiceId, patientId) in all subsequent log messages.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

… 1040-load-balance-athena

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Thomas Yopes added 3 commits March 10, 2025 13:45
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
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: 0

🧹 Nitpick comments (3)
packages/lambdas/src/shared/ehr.ts (3)

23-25: Consider using nullish check instead of undefined comparison.

The coding guidelines suggest: "Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value", which implies a preference for modern JavaScript nullish handling.

For consistency with modern JavaScript practices, consider using a nullish check instead:

-  if (triggerDqRaw === undefined) throw new Error(`Missing triggerDq`);
+  if (triggerDqRaw == null) throw new Error(`Missing triggerDq`);

This would catch both undefined and null values, which is generally safer.


1-34: Add function documentation with JSDoc comments.

The function lacks documentation that explains its purpose, parameters, return value, and potential errors.

Add JSDoc comments to improve code maintainability:

+/**
+ * Parses and validates an incoming request for syncing patient data.
+ * 
+ * @param bodyAsJson - The raw JSON body from the request
+ * @returns A validated ProcessSyncPatientRequest object
+ * @throws MetriportError when validation fails
+ */
export function parseSyncPatient(bodyAsJson: any): ProcessSyncPatientRequest {

13-13: Consider providing more specific error message for invalid EHR source.

The error message for an invalid EHR source is the same as for an EHR that's not a string, which might make debugging more difficult.

Make the error message more specific to help with debugging:

-  if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr`);
+  if (!isEhrSource(ehrRaw)) throw new Error(`Invalid ehr source: ${ehrRaw}`);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f4efcad and 62a79ee.

📒 Files selected for processing (2)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lambdas/src/ehr-sync-patient.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/shared/ehr.ts
🔇 Additional comments (2)
packages/lambdas/src/shared/ehr.ts (2)

4-5: Replace any type with proper interfaces for better type safety.

Using any type violates the TypeScript guidelines in your coding standards, which state "Avoid any and casting from any to other types".

Consider creating a specific interface for the input payload:

import { ProcessSyncPatientRequest } from "@metriport/core/command/ehr/sync-patient/ehr-sync-patient";
import { isEhrSource } from "@metriport/core/src/external/shared/ehr";

+interface SyncPatientPayload {
+  cxId: unknown;
+  ehr: unknown;
+  practiceId: unknown;
+  patientId: unknown;
+  triggerDq: unknown;
+}

-//eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function parseSyncPatient(bodyAsJson: any): ProcessSyncPatientRequest {
+export function parseSyncPatient(bodyAsJson: unknown): ProcessSyncPatientRequest {
+  const payload = bodyAsJson as SyncPatientPayload;

6-25: Improve error handling by using MetriportError with additional context.

The current error handling uses plain Error objects without additional context, which makes debugging harder. According to your coding guidelines, "Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop".

Enhance error handling with MetriportError to provide more context:

import { ProcessSyncPatientRequest } from "@metriport/core/command/ehr/sync-patient/ehr-sync-patient";
import { isEhrSource } from "@metriport/core/src/external/shared/ehr";
+import { MetriportError } from "@metriport/shared";

// Then update the error handling in validation blocks:
-  if (!cxIdRaw) throw new Error(`Missing cxId`);
-  if (typeof cxIdRaw !== "string") throw new Error(`Invalid cxId`);
+  if (!cxIdRaw) throw new MetriportError(`Missing cxId`, undefined, { providedValues: bodyAsJson });
+  if (typeof cxIdRaw !== "string") throw new MetriportError(`Invalid cxId`, undefined, { providedValue: cxIdRaw });

// Do the same for other validation blocks

Thomas Yopes added 8 commits March 10, 2025 15:38
… 1040-load-balance-athena

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

🧹 Nitpick comments (3)
packages/core/src/external/ehr/api/sync-patient.ts (3)

29-29: Update log context to match function name

The log context uses "PatientImport createPatient" but this function is named "syncPatient". The context should match the function name for better traceability.

- const { log, debug } = out(`PatientImport createPatient - cxId ${cxId}`);
+ const { log, debug } = out(`syncPatient - cxId ${cxId}`);

41-41: Avoid potential multi-line logs

According to coding guidelines, multi-line logs should be avoided. Using JSON.stringify() on response data could result in multi-line output.

- debug(`${syncPatientUrl} resp: ${JSON.stringify(response.data)}`);
+ debug(`${syncPatientUrl} request succeeded`);

Or if debugging specific fields is needed:

- debug(`${syncPatientUrl} resp: ${JSON.stringify(response.data)}`);
+ debug(`${syncPatientUrl} response status: ${response.status}`);

43-43: Improve error message specificity

The error message "Failure while syncing patient @ Ehr" is generic. Consider making it more specific by including the actual EHR source name.

- const msg = `Failure while syncing patient @ Ehr`;
+ const msg = `Failure while syncing patient with ${ehr}`;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b1a7879 and 680674e.

📒 Files selected for processing (20)
  • packages/api/src/command/mapping/cx.ts (2 hunks)
  • packages/api/src/domain/cx-mapping.ts (1 hunks)
  • packages/api/src/domain/facility-mapping.ts (1 hunks)
  • packages/api/src/domain/patient-mapping.ts (1 hunks)
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/athenahealth/command/sync-patient.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/canvas/command/sync-patient.ts (1 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/elation/command/sync-patient.ts (1 hunks)
  • packages/api/src/external/ehr/patient.ts (1 hunks)
  • packages/api/src/external/ehr/shared-fhir.ts (1 hunks)
  • packages/api/src/external/ehr/shared.ts (3 hunks)
  • packages/core/src/external/ehr/api/sync-patient.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/index.ts (3 hunks)
  • packages/core/src/external/ehr/canvas/index.ts (2 hunks)
  • packages/core/src/external/ehr/elation/index.ts (3 hunks)
  • packages/core/src/external/ehr/shared.ts (2 hunks)
  • packages/core/src/external/ehr/sync-patient/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/api/src/domain/patient-mapping.ts
  • packages/api/src/domain/facility-mapping.ts
  • packages/api/src/external/ehr/shared-fhir.ts
  • packages/api/src/command/mapping/cx.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/core/src/external/ehr/sync-patient/ehr-sync-patient.ts
  • packages/api/src/external/ehr/athenahealth/command/sync-patient.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
  • packages/core/src/external/ehr/canvas/index.ts
  • packages/api/src/external/ehr/canvas/command/sync-patient.ts
  • packages/core/src/external/ehr/elation/index.ts
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
  • packages/api/src/external/ehr/elation/command/sync-patient.ts
  • packages/api/src/external/ehr/patient.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's c...

**/*.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/shared/ehr.ts
  • packages/api/src/domain/cx-mapping.ts
  • packages/core/src/external/ehr/api/sync-patient.ts
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
  • packages/api/src/external/ehr/shared.ts
🧠 Learnings (1)
packages/core/src/external/ehr/api/sync-patient.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/core/src/external/ehr/api/sync-patient.ts:16-55
Timestamp: 2025-03-12T15:29:01.738Z
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.
🔇 Additional comments (14)
packages/core/src/external/ehr/api/sync-patient.ts (1)

16-55: Overall implementation aligns with architecture design

The implementation correctly follows the architecture pattern described in the PR:

  1. This function is meant to be called by the Lambda that processes messages from the queue
  2. It makes the API call to perform the actual patient synchronization
  3. It provides appropriate error handling and logging

The function correctly implements the queue-based processing approach outlined in the PR objectives.

packages/lambdas/src/shared/ehr.ts (3)

1-12: Well-structured interface and imports for the sync patient functionality.

The implementation correctly imports necessary dependencies and defines a proper interface with unknown types that will be validated in the function. This follows the TypeScript guidelines in your coding standards by avoiding any types.


13-34: Good error handling implementation with proper validation.

The function performs thorough validation of all required fields and follows best practices:

  • Explicit type checks for each property
  • Using MetriportError for error handling with clear messages
  • Checking for existence before type validation

The validation for the EHR source is particularly valuable to ensure only valid sources are accepted.


35-42: Clean and type-safe return implementation.

The function returns a properly typed object with validated properties, ensuring type safety throughout the application. This maintains the functional programming style recommended in your coding guidelines.

packages/api/src/domain/cx-mapping.ts (2)

2-7: Improved imports with centralized EHR source definitions.

The code now imports from a more organized structure, pulling in ehrSources and the EHR-specific mapping types from a centralized location, which improves maintainability.


15-18: Consolidated secondary mappings with a more modular approach.

The updated type definition and schema map now use a more extensible pattern with EhrCxMappingSecondaryMappings and the spread operator for the schema map. This allows for better scaling when adding new EHR integrations.

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

2-9: Updated imports to support asynchronous patient synchronization.

The code now imports buildEhrSyncPatientHandler and related components to enable the new asynchronous processing model for patient events, aligning with the PR objective of sending sync patient events to a queue.


17-18: Using standardized delay constant for patient batch processing.

The code now references delayBetweenPatientBatches from the shared module, providing consistent timing across different EHR integrations.


100-103: Simplified patient synchronization with asynchronous processing.

The implementation has been streamlined to directly call executeAsynchronously with the sync function, without collecting errors. This aligns with the asynchronous nature of the new implementation where errors are handled within the queue processing.


183-196: Refactored patient sync function to use the handler factory pattern.

The syncPatient function has been updated to:

  1. Remove the triggerDq parameter from the function signature
  2. Use buildEhrSyncPatientHandler() to get the appropriate handler
  3. Pass the sync parameters to the handler with triggerDq set to true

This implementation cleanly separates the synchronization logic from the execution details, following good software design principles.

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

1-9: Updated import paths for better code organization.

The imports have been reorganized to reflect a more structured approach to the EHR integration modules, importing from specific locations rather than using generic paths.


39-41: Added patient batch delay and significantly increased parallel processing.

The new configuration introduces:

  1. A standardized 1-second delay between patient batches
  2. A substantial increase in parallel patient processing from 2 to 200

While this change aligns with the goal of implementing queue-based processing, the 100x increase in parallelism might impact system performance.

Please verify that the system can handle 200 parallel operations without performance degradation or resource exhaustion, especially in production environments with high patient volumes.


56-62: Streamlined JWT token source definitions.

The EHR dashboard JWT token sources are now defined more directly using the imported constants, which improves code readability and maintenance.


87-94: Added EHR mapping schema structure for better integration support.

The new type and schema map for EHR-specific mappings provides a structured approach to handling different EHR integrations with their unique secondary mapping requirements.

Thomas Yopes added 4 commits March 12, 2025 09:39
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 1040-load-balance-athena

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes added this pull request to the merge queue Mar 12, 2025
Merged via the queue into develop with commit 09b2221 Mar 12, 2025
21 checks passed
@thomasyopes thomasyopes deleted the 1040-load-balance-athena branch March 12, 2025 17:16
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