Repository: metriport/metriport
Pull request #3716: Eng 89 hl7 to fhir converter lambda
Author: login: RamilGaripov
Created: 2025-04-24T01:58:22Z | Merged: 2025-04-25T02:47:56Z

Issues:

- https://linear.app/metriport/issue/ENG-89

### Description
- replaced the logic for the HL7 webhook sender lambda
- adding env var retrieval to HL7 webhook sender lambda
- updated local script for testing

### Testing

- Local
  - [x] use the local script to invoke direct lambda logic and check result on s3
- Staging
  - [ ] TBD
- Production
  - [ ] TBD

### Release Plan
- [ ] Merge this


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved HL7 message handling by dynamically constructing internal API URLs and supporting robust patient data retrieval with retry logic.
  - Enhanced FHIR bundle creation by prepending patient resources and standardizing HL7 and FHIR file key generation with fixed extensions.
  - Added support for generating presigned URLs for uploaded bundles and notifying internal endpoints with enriched logging.

- **Refactor**
  - Streamlined configuration by passing API URL and bucket name to relevant components and functions.
  - Simplified file key generation by removing variable extension parameters and using standardized file extensions.

- **Chores**
  - Updated environment variable usage and import organization for improved maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts b/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
index d82142d7e2..4925548ceb 100644
--- a/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
+++ b/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
@@ -1,65 +1,129 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
-  private readonly s3Utils: S3Utils;
+  private readonly context = "hl7-notification-wh-sender";
 
-  constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
-    this.s3Utils = new S3Utils(Config.getAWSRegion());
-  }
+  constructor(
+    private readonly apiUrl: string,
+    private readonly bucketName: string,
+    private readonly s3Utils = new S3Utils(Config.getAWSRegion())
+  ) {}
 
   async execute(params: Hl7Notification): Promise<void> {
     const message = Hl7Message.parse(params.message);
     const { cxId, patientId, messageReceivedTimestamp } = params;
+    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);
+
     const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    if (!supportedTypes.includes(triggerEvent)) {
+      log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+      return;
+    }
+
+    const internalHl7RouteUrl = `${this.apiUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
+    const internalGetPatientUrl = `${this.apiUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
+
+    const patient = await executeWithNetworkRetries(
+      async () => await axios.get(internalGetPatientUrl)
+    );
+    const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
+
+    const conversionResult = convertHl7v2MessageToFhir({
+      message,
+      cxId,
+      patientId,
+      timestampString: messageReceivedTimestamp,
+    });
+
+    const bundle = prependPatientToBundle({
+      bundle: conversionResult,
+      fhirPatient,
+    });
+    log(`Conversion complete and patient entry added`);
 
-    const fileKey = buildHl7MessageFileKey({
+    const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
+      s3Utils: this.s3Utils,
+      bucketName: this.bucketName,
+      contentType: JSON_APP_MIME_TYPE,
+      log,
+      errorConfig: {
+        errorMessage: "Error uploading HL7 FHIR bundle to S3",
+        context: this.context,
+        captureParams: {
+          patientId,
+          cxId,
+          messageReceivedTimestamp,
+        },
+        shouldCapture: true,
+      },
+    };
+
+    const newBundleFileName = buildHl7MessageFhirBundleFileKey({
       cxId,
       patientId,
       timestamp: messageReceivedTimestamp,
       messageId: getMessageUniqueIdentifier(message),
       messageCode,
       triggerEvent,
-      extension: "json",
     });
 
-    const convertedMessage = convertHl7v2MessageToFhir({
-      cxId,
-      patientId,
-      message,
-      timestampString: messageReceivedTimestamp,
+    await storeInS3WithRetries({
+      ...nonSpecificUploadParams,
+      payload: JSON.stringify(bundle),
+      fileName: newBundleFileName,
     });
+    log(`Uploaded to S3. Filepath: ${newBundleFileName}`);
 
-    this.log(`${messageReceivedTimestamp} - cx: ${cxId} - pt: ${patientId} - Uploading file to S3`);
-
-    await this.s3Utils.uploadFile({
-      bucket: this.bucketName,
-      key: fileKey,
-      file: Buffer.from(JSON.stringify(convertedMessage)),
-      contentType: "application/json",
+    const bundlePresignedUrl = await this.s3Utils.getSignedUrl({
+      bucketName: this.bucketName,
+      fileName: newBundleFileName,
+      durationSeconds: SIGNED_URL_DURATION_SECONDS,
     });
 
-    const api = axios.create({ baseURL: Config.getApiUrl() });
-    this.log(
-      `[${messageReceivedTimestamp}] Sending webhook for cxId ${cxId} + patientId ${patientId}`
+    await executeWithNetworkRetries(
+      async () =>
+        await axios.post(internalHl7RouteUrl, undefined, {
+          params: {
+            cxId,
+            triggerEvent,
+            presignedUrl: bundlePresignedUrl,
+          },
+        })
     );
-    const response = await api.get("/");
-    this.log(`[${messageReceivedTimestamp}] response: ${JSON.stringify(response.data)}`);
+    log(`Done. API notified...`);
   }
 }
+
+function prependPatientToBundle({
+  bundle,
+  fhirPatient,
+}: {
+  bundle: Bundle<Resource>;
+  fhirPatient: Resource;
+}): Bundle<Resource> {
+  const fhirPatientEntry = buildBundleEntry(fhirPatient);
+  const combinedEntries = bundle.entry ? [fhirPatientEntry, ...bundle.entry] : [];
+  return buildBundle({ type: "collection", entries: combinedEntries });
+}
diff --git a/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts b/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts
index 23c2391bce..4215f7ff6b 100644
--- a/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts
+++ b/packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts
@@ -4,6 +4,11 @@ import { Hl7NotificationWebhookSenderCloud } from "./hl7-notification-webhook-se
 import { Hl7NotificationWebhookSenderDirect } from "./hl7-notification-webhook-sender-direct";
 
 export function buildHl7NotificationWebhookSender(): Hl7NotificationWebhookSender {
-  if (Config.isDev()) return new Hl7NotificationWebhookSenderDirect();
+  if (Config.isDev()) {
+    return new Hl7NotificationWebhookSenderDirect(
+      Config.getApiLoadBalancerAddress(),
+      Config.getHl7OutgoingMessageBucketName()
+    );
+  }
   return new Hl7NotificationWebhookSenderCloud(Config.getHl7NotificationQueueUrl());
 }
diff --git a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/index.ts b/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/index.ts
index a52e081993..ff544b1df9 100644
--- a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/index.ts
+++ b/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/index.ts
@@ -39,7 +39,6 @@ export function convertHl7v2MessageToFhir({
     messageId,
     messageCode,
     triggerEvent,
-    extension: "hl7",
   });
 
   if (messageCode === "ADT") {
diff --git a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts b/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
index 71ab8e03b9..85c7309967 100644
--- a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
+++ b/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
@@ -5,7 +5,7 @@ import { capture, out } from "../../../util";
 import { Base64Scrambler } from "../../../util/base64-scrambler";
 import { Config } from "../../../util/config";
 import { ICD_10_URL, ICD_9_URL, LOINC_URL, SNOMED_URL } from "../../../util/constants";
-import { JSON_FILE_EXTENSION } from "../../../util/mime";
+import { HL7_FILE_EXTENSION, JSON_FILE_EXTENSION } from "../../../util/mime";
 import { packUuid, unpackUuid } from "../../../util/pack-uuid";
 import { getMessageDatetime, getMessageUniqueIdentifier } from "./msh";
 
@@ -16,7 +16,6 @@ type Hl7FileKeyParams = {
   timestamp: string;
   messageCode: string;
   triggerEvent: string;
-  extension: "hl7" | "json";
 };
 
 // TODO: Ensure the HL7 coding system values are correct and up to date
@@ -154,9 +153,8 @@ export function buildHl7MessageFileKey({
   timestamp,
   messageCode,
   triggerEvent,
-  extension,
 }: Hl7FileKeyParams) {
-  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}.${extension}`;
+  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}.${HL7_FILE_EXTENSION}`;
 }
 
 export function formatDateToHl7(date: Date): string {
diff --git a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-converter.ts b/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-converter.ts
deleted file mode 100644
index 393028fce6..0000000000
--- a/packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-converter.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { Hl7Message } from "@medplum/core";
-import { Bundle, Resource } from "@medplum/fhirtypes";
-import { executeWithNetworkRetries } from "@metriport/shared";
-import axios from "axios";
-import dayjs from "dayjs";
-import duration from "dayjs/plugin/duration";
-import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
-import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
-import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
-import { out } from "../../util";
-import { Config } from "../../util/config";
-import { JSON_APP_MIME_TYPE } from "../../util/mime";
-import { convertHl7v2MessageToFhir } from "./hl7v2-to-fhir-conversion";
-import { getHl7MessageTypeOrFail } from "./hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFhirBundleFileKey } from "./hl7v2-to-fhir-conversion/shared";
-
-dayjs.extend(duration);
-
-const region = Config.getAWSRegion();
-
-const supportedTypes = ["A01", "A03"];
-const INTERNAL_HL7_ENDPOINT = `notification`;
-const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
-const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
-
-type Hl7ToFhirLambdaProps = {
-  cxId: string;
-  patientId: string;
-  message: string;
-  messageId: string;
-  messageReceivedTimestamp: string;
-  apiUrl?: string;
-  bucketName?: string;
-};
-
-function getS3UtilsInstance(): S3Utils {
-  return new S3Utils(region);
-}
-
-export async function convertHl7MessageToFhirAndUpload({
-  cxId,
-  patientId,
-  message,
-  messageId,
-  messageReceivedTimestamp,
-  apiUrl,
-  bucketName,
-}: Hl7ToFhirLambdaProps): Promise<void> {
-  const baseUrl = apiUrl || Config.getApiLoadBalancerAddress();
-  const s3BucketName = bucketName || Config.getHl7OutgoingMessageBucketName();
-
-  const { log } = out(`Hl7 to FHIR Lambda - cx: ${cxId}, pt: ${patientId}`);
-  log(`Converting message from ${messageReceivedTimestamp}`);
-
-  const hl7Message = Hl7Message.parse(message);
-  const s3Client = getS3UtilsInstance();
-  const internalHl7RouteUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
-  const internalGetPatientUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
-
-  const msgType = getHl7MessageTypeOrFail(hl7Message);
-  if (!supportedTypes.includes(msgType.triggerEvent)) {
-    log(`Message type ${msgType.triggerEvent} is not supported. Skipping...`);
-    return;
-  }
-
-  const patient = await executeWithNetworkRetries(
-    async () => await axios.get(internalGetPatientUrl)
-  );
-  const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
-
-  const convertedBundle = convertHl7v2MessageToFhir({
-    message: hl7Message,
-    cxId,
-    patientId,
-    timestampString: messageReceivedTimestamp,
-  });
-
-  const bundle = saturateConvertedBundle({
-    bundle: convertedBundle,
-    fhirPatient,
-  });
-
-  const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
-    s3Utils: s3Client,
-    bucketName: s3BucketName,
-    contentType: JSON_APP_MIME_TYPE,
-    log,
-    errorConfig: {
-      errorMessage: "Error uploading HL7 FHIR bundle to S3",
-      context: "convertHl7MessageToFhirAndUpload",
-      captureParams: {
-        patientId,
-        cxId,
-        messageReceivedTimestamp,
-      },
-      shouldCapture: true,
-    },
-  };
-
-  const newBundleFileName = buildHl7MessageFhirBundleFileKey({
-    cxId,
-    patientId,
-    timestamp: messageReceivedTimestamp,
-    messageId,
-    messageCode: msgType.messageCode,
-    triggerEvent: msgType.triggerEvent,
-    extension: "hl7",
-  });
-
-  await storeInS3WithRetries({
-    ...nonSpecificUploadParams,
-    payload: JSON.stringify(bundle),
-    fileName: newBundleFileName,
-  });
-
-  log(`Conversion complete, and result uploaded to S3. File: ${newBundleFileName}`);
-
-  const bundlePresignedUrl = await s3Client.getSignedUrl({
-    bucketName: s3BucketName,
-    fileName: newBundleFileName,
-    durationSeconds: SIGNED_URL_DURATION_SECONDS,
-  });
-
-  await executeWithNetworkRetries(
-    async () =>
-      await axios.post(internalHl7RouteUrl, undefined, {
-        params: {
-          cxId,
-          triggerEvent: msgType.triggerEvent,
-          presignedUrl: bundlePresignedUrl,
-        },
-      })
-  );
-  log(`Done. API notified...`);
-}
-
-function saturateConvertedBundle({
-  bundle,
-  fhirPatient,
-}: {
-  bundle: Bundle<Resource>;
-  fhirPatient: Resource;
-}): Bundle<Resource> {
-  const fhirPatientEntry = buildBundleEntry(fhirPatient);
-  const combinedEntries = bundle.entry ? [fhirPatientEntry, ...bundle.entry] : [];
-  return buildBundle({ type: "collection", entries: combinedEntries });
-}
diff --git a/packages/core/src/util/mime.ts b/packages/core/src/util/mime.ts
index 965c341d98..2b639a034e 100644
--- a/packages/core/src/util/mime.ts
+++ b/packages/core/src/util/mime.ts
@@ -43,6 +43,7 @@ export const OCTET_FILE_EXTENSION = ".bin";
 export const HTML_MIME_TYPE = "text/html";
 export const HTML_FILE_EXTENSION = ".html";
 export const TXT_RTF_MIME_TYPE = "text/rtf";
+export const HL7_FILE_EXTENSION = "hl7";
 
 export const BINARY_MIME_TYPES = [
   PDF_MIME_TYPE,
diff --git a/packages/lambdas/src/hl7-notification-webhook-sender.ts b/packages/lambdas/src/hl7-notification-webhook-sender.ts
index 3a93742020..2e52205eb1 100644
--- a/packages/lambdas/src/hl7-notification-webhook-sender.ts
+++ b/packages/lambdas/src/hl7-notification-webhook-sender.ts
@@ -1,18 +1,20 @@
 import { Hl7Notification } from "@metriport/core/command/hl7-notification/hl7-notification-webhook-sender";
-import { SQSEvent } from "aws-lambda";
 import { Hl7NotificationWebhookSenderDirect } from "@metriport/core/command/hl7-notification/hl7-notification-webhook-sender-direct";
-import { capture } from "./shared/capture";
 import * as Sentry from "@sentry/serverless";
-import { getSingleMessageOrFail } from "./shared/sqs";
-import { getEnvOrFail } from "./shared/env";
+import { SQSEvent } from "aws-lambda";
 import { z } from "zod";
+import { capture } from "./shared/capture";
+import { getEnvOrFail } from "./shared/env";
 import { prefixedLog } from "./shared/log";
+import { getSingleMessageOrFail } from "./shared/sqs";
 
 // Keep this as early on the file as possible
 capture.init();
 
 // Automatically set by AWS
 const lambdaName = getEnvOrFail("AWS_LAMBDA_FUNCTION_NAME");
+const bucketName = getEnvOrFail("HL7_OUTGOING_MESSAGE_BUCKET_NAME");
+const apiUrl = getEnvOrFail("API_URL");
 
 export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent): Promise<void> => {
   const params = getSingleMessageOrFail(event.Records, lambdaName);
@@ -31,7 +33,7 @@ export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent): Pro
     context: "hl7-notification-webhook-sender-cloud.execute",
   });
 
-  await new Hl7NotificationWebhookSenderDirect().execute(parsedBody);
+  await new Hl7NotificationWebhookSenderDirect(apiUrl, bucketName).execute(parsedBody);
 });
 
 const parseBody = (body: string): Hl7Notification => {
diff --git a/packages/mllp-server/src/app.ts b/packages/mllp-server/src/app.ts
index 9b41eeb0b3..9ea435fef3 100644
--- a/packages/mllp-server/src/app.ts
+++ b/packages/mllp-server/src/app.ts
@@ -75,7 +75,6 @@ async function createHl7Server(logger: Logger): Promise<Hl7Server> {
             messageId,
             messageCode,
             triggerEvent,
-            extension: "hl7",
           }),
           file: Buffer.from(asString(message)),
           contentType: "text/plain",
diff --git a/packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts b/packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts
index 1a9a061f87..374de28e39 100644
--- a/packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts
+++ b/packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts
@@ -2,12 +2,9 @@ import * as dotenv from "dotenv";
 dotenv.config();
 // keep that ^ on top
 import { Hl7Message } from "@medplum/core";
-import {
-  getMessageUniqueIdentifier,
-  getOrCreateMessageDatetime,
-} from "@metriport/core/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
+import { Hl7NotificationWebhookSenderDirect } from "@metriport/core/command/hl7-notification/hl7-notification-webhook-sender-direct";
+import { getOrCreateMessageDatetime } from "@metriport/core/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
 import { getCxIdAndPatientIdOrFail } from "@metriport/core/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
-import { convertHl7MessageToFhirAndUpload } from "@metriport/core/command/hl7v2-subscriptions/hl7v2-to-fhir-converter";
 import { errorToString, getEnvVarOrFail } from "@metriport/shared";
 import fs from "fs";
 
@@ -58,18 +55,14 @@ function invokeLambdaLogic() {
   chunks.forEach(message => {
     const hl7Message = Hl7Message.parse(message);
     const timestamp = getOrCreateMessageDatetime(hl7Message);
-    const messageId = getMessageUniqueIdentifier(hl7Message);
 
     try {
       const { cxId, patientId } = getCxIdAndPatientIdOrFail(hl7Message);
-      convertHl7MessageToFhirAndUpload({
+      new Hl7NotificationWebhookSenderDirect(apiUrl, bucketName).execute({
         cxId,
         patientId,
         message,
-        messageId,
         messageReceivedTimestamp: timestamp,
-        apiUrl,
-        bucketName,
       });
     } catch (err) {
       errors.push({


Reviews:
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T02:02:00Z
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 8**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts (2)</summary><blockquote>

`80-83`: **Avoid multi-line logs per coding guidelines.**

The coding guidelines specifically discourage multi-line logs and using `JSON.stringify()` in logs. Consider using a structured logging approach instead.

```diff
-  if (errors.length > 0) {
-    console.log("ERRORS ARE:");
-    console.log(JSON.stringify(errors, null, 2));
-  }
+  if (errors.length > 0) {
+    console.log(`Encountered ${errors.length} errors during processing`);
+    errors.forEach((error, index) => {
+      console.log(`Error ${index + 1}: ${error.msg} (Timestamp: ${error.timestamp})`);
+    });
+  }
```

---

`86-86`: **Make script execution more configurable.**

The script immediately executes upon import, which limits its reusability and makes it harder to test. Consider making it a module that can be imported and executed deliberately.

```diff
-invokeLambdaLogic();
+// Only execute directly if this file is being run directly (not imported)
+if (require.main === module) {
+  invokeLambdaLogic()
+    .then(success => {
+      process.exit(success ? 0 : 1);
+    })
+    .catch(err => {
+      console.error(`Unhandled error: ${errorToString(err)}`);
+      process.exit(1);
+    });
+}
+
+// Export for potential reuse in tests or other modules
+export { invokeLambdaLogic };
```

</blockquote></details>
<details>
<summary>packages/api/src/routes/medical/schemas/hl7-notification.ts (2)</summary><blockquote>

`7-20`: **Strengthen S3 URL validation.**

The current validation only checks if the hostname contains "s3" anywhere, which could match non-AWS domains. Consider using a more specific pattern to validate AWS S3 URLs.

```diff
 export const presignedUrlSchema = z
   .string()
   .url()
   .refine(
     url => {
       try {
         const urlObj = new URL(url);
-        return urlObj.hostname.includes("s3");
+        // Match more specific S3 URL patterns (e.g., s3.amazonaws.com or s3.[region].amazonaws.com)
+        return /^s3[.-]([a-z0-9-]+\.)?amazonaws\.com$/.test(urlObj.hostname);
       } catch {
         return false;
       }
     },
-    { message: "URL must be a valid S3 url" }
+    { message: "URL must be a valid AWS S3 presigned URL" }
   );
```

---

`25-29`: **Consider adding enum validation for triggerEvent.**

Currently, `triggerEvent` accepts any string. Adding an enum of valid trigger events would improve type safety and provide better error messages.

```diff
+// Define valid trigger events
+export const triggerEvents = ["ADT", "ORU", "MDM", "SIU"] as const;
+
 export const hl7NotificationSchema = z.object({
   cxId: z.string().uuid(),
   presignedUrl: presignedUrlSchema,
-  triggerEvent: z.string(),
+  triggerEvent: z.enum(triggerEvents, {
+    errorMap: () => ({ message: `triggerEvent must be one of: ${triggerEvents.join(", ")}` }),
+  }),
 });
+
+// Export the type for use in TypeScript
+export type Hl7Notification = z.infer<typeof hl7NotificationSchema>;
```

</blockquote></details>
<details>
<summary>packages/utils/src/hl7v2-notifications/convert-adt-to-fhir-local-example.ts (1)</summary><blockquote>

`69-72`: **Micro-optimisation: create `converted` folder once**  

`fs.existsSync(outputFolder)` is executed for **every message**.  
Move the check outside the inner loop to avoid redundant I/O.

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (8)</summary><blockquote>

<details>
<summary>packages/api/src/command/medical/patient/hl7-fhir-webhook.ts (2)</summary><blockquote>

10-10: _⚠️ Potential issue_

**Unused parameter in function signature**

The `presignedUrl` parameter is defined in the function signature but not used in the implementation. While this is expected given the TODO status, ensure this is implemented when addressing issue #2888.

---

3-14: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add implementation for processing the FHIR bundle from presigned URL**

The function contains only logging but lacks actual implementation to process the FHIR bundle from the presigned URL. This aligns with the TODO comment, but should be prioritized to complete the webhook flow.


Verify that issue #2888 is properly tracked in your project management system:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the issue is tracked in GitHub or other visible issues
gh issue view 2888 || echo "Issue might be tracked in another system"
```

Length of output: 481

---



</details>

**Implement FHIR bundle processing and correct TODO reference**

- The `processHl7FhirBundleWebhook` function is still a stub—it only logs the presigned URL. Please implement the logic to fetch the bundle from `presignedUrl`, parse the FHIR resources, and invoke the downstream processing steps.
- The `// TODO 2888:…` comment points to issue #2888 (duplicate performer references), which has already been merged and isn’t related to this webhook. Update the TODO to reference the correct issue for this work or remove it if no issue exists.

</blockquote></details>
<details>
<summary>packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts (3)</summary><blockquote>

47-48: _⚠️ Potential issue_

**Initialize file path and name with meaningful defaults or add validation.**

Empty string constants for `filePath` and `fileName` will cause runtime errors when attempting to read the file. Either provide meaningful defaults or add validation before file access.

```diff
-const filePath = "";
-const fileName = "";
+const filePath = process.env.HL7_FILE_PATH || "./data";
+const fileName = process.env.HL7_FILE_NAME || "sample.hl7";
+
+if (!fs.existsSync(`${filePath}/${fileName}`)) {
+  console.error(`File not found: ${filePath}/${fileName}`);
+  process.exit(1);
+}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
-const filePath = "";
-const fileName = "";
+const filePath = process.env.HL7_FILE_PATH || "./data";
+const fileName = process.env.HL7_FILE_NAME || "sample.hl7";
+
+if (!fs.existsSync(`${filePath}/${fileName}`)) {
+  console.error(`File not found: ${filePath}/${fileName}`);
+  process.exit(1);
+}
`````

</details>

<!-- suggestion_end -->

---

63-70: _🛠️ Refactor suggestion_

**Add async/await for proper promise handling.**

The `execute` method likely returns a Promise, but it's not being awaited. This could lead to unhandled promise rejections and race conditions when processing multiple messages.

```diff
-      hl7WebhookLambda.execute({
-        cxId,
-        patientId,
-        message,
-        messageReceivedTimestamp: timestamp,
-        apiUrl,
-        bucketName,
-      });
+      await hl7WebhookLambda.execute({
+        cxId,
+        patientId,
+        message,
+        messageReceivedTimestamp: timestamp,
+        apiUrl,
+        bucketName,
+      });
```

This change requires making the `invokeLambdaLogic` function async and handling the returned Promise when calling it.


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

---

50-84: _🛠️ Refactor suggestion_

**Make the function more robust with proper error handling.**

The current implementation has several areas for improvement:
1. It doesn't handle file read errors
2. It doesn't validate message content before parsing
3. It doesn't provide a clear success/failure indication


Consider refactoring to handle these cases:

```diff
-function invokeLambdaLogic() {
+async function invokeLambdaLogic() {
   const hl7WebhookLambda = buildHl7NotificationWebhookSender();
+  const errors: unknown[] = [];
+  let processedCount = 0;
+  
+  try {
+    const hl7Text = fs.readFileSync(`${filePath}/${fileName}`, "utf-8");
+    const chunks = hl7Text.split(/(?=^MSH\|)/m).filter(chunk => chunk.trim());
+    
+    console.log(`Processing ${chunks.length} HL7 messages...`);
-
-  const hl7Text = fs.readFileSync(`${filePath}/${fileName}`, "utf-8");
-  const chunks = hl7Text.split(/(?=^MSH\|)/m);
-
-  const errors: unknown[] = [];
-  chunks.forEach(message => {
+    for (const message of chunks) {
+      if (!message.startsWith("MSH|")) {
+        errors.push({
+          err: new Error("Invalid HL7 message format"),
+          msg: "Message does not start with MSH segment",
+          timestamp: new Date().toISOString(),
+        });
+        continue;
+      }
+      
+      try {
         const hl7Message = Hl7Message.parse(message);
         const timestamp = getOrCreateMessageDatetime(hl7Message);
-
-    try {
         const { cxId, patientId } = getCxIdAndPatientIdOrFail(hl7Message);
-      hl7WebhookLambda.execute({
+        await hl7WebhookLambda.execute({
           cxId,
           patientId,
           message,
           messageReceivedTimestamp: timestamp,
           apiUrl,
           bucketName,
         });
+        processedCount++;
       } catch (err) {
         errors.push({
           err,
           msg: errorToString(err),
           timestamp,
         });
       }
-  });
+    }
+  } catch (err) {
+    console.error(`Failed to read or process file: ${errorToString(err)}`);
+    return false;
+  }

   if (errors.length > 0) {
-    console.log("ERRORS ARE:");
-    console.log(JSON.stringify(errors, null, 2));
+    console.log(`Processed ${processedCount} messages with ${errors.length} errors`);
+    errors.forEach((error, index) => {
+      console.log(`Error ${index + 1}: ${(error as any).msg}`);
+    });
+    return false;
   }
+  
+  console.log(`Successfully processed ${processedCount} messages`);
+  return true;
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
async function invokeLambdaLogic() {
  const hl7WebhookLambda = buildHl7NotificationWebhookSender();
  const errors: unknown[] = [];
  let processedCount = 0;
  
  try {
    const hl7Text = fs.readFileSync(`${filePath}/${fileName}`, "utf-8");
    const chunks = hl7Text
      .split(/(?=^MSH\|)/m)
      .filter(chunk => chunk.trim());

    console.log(`Processing ${chunks.length} HL7 messages...`);

    for (const message of chunks) {
      if (!message.startsWith("MSH|")) {
        errors.push({
          err: new Error("Invalid HL7 message format"),
          msg: "Message does not start with MSH segment",
          timestamp: new Date().toISOString(),
        });
        continue;
      }

      try {
        const hl7Message = Hl7Message.parse(message);
        const timestamp = getOrCreateMessageDatetime(hl7Message);
        const { cxId, patientId } = getCxIdAndPatientIdOrFail(hl7Message);

        await hl7WebhookLambda.execute({
          cxId,
          patientId,
          message,
          messageReceivedTimestamp: timestamp,
          apiUrl,
          bucketName,
        });
        processedCount++;
      } catch (err) {
        errors.push({
          err,
          msg: errorToString(err),
          timestamp,
        });
      }
    }
  } catch (err) {
    console.error(`Failed to read or process file: ${errorToString(err)}`);
    return false;
  }

  if (errors.length > 0) {
    console.log(`Processed ${processedCount} messages with ${errors.length} errors`);
    errors.forEach((error, index) => {
      console.log(`Error ${index + 1}: ${(error as any).msg}`);
    });
    return false;
  }

  console.log(`Successfully processed ${processedCount} messages`);
  return true;
}
`````

</details>

<!-- suggestion_end -->

</blockquote></details>
<details>
<summary>packages/utils/src/hl7v2-notifications/convert-adt-to-fhir-local-example.ts (2)</summary><blockquote>

54-58: _⚠️ Potential issue_

**Empty chunks crash `Hl7Message.parse` – filter them out**  

Splitting with a zero-width look-ahead (`split(/(?=^MSH\|)/m)`) produces a **leading empty string** when the file starts with `MSH|`.  
Passing `""` to `Hl7Message.parse()` throws and pollutes the error log for every file.

```diff
-const chunks = hl7Text.split(/(?=^MSH\|)/m);
+const chunks = hl7Text
+  .split(/(?=^MSH\|)/m)
+  .map(c => c.trim())
+  .filter(Boolean); // discard empty chunks
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    const chunks = hl7Text
      .split(/(?=^MSH\|)/m)
      .map(c => c.trim())
      .filter(Boolean); // discard empty chunks

    chunks.forEach((msg, index) => {
      const hl7Message = Hl7Message.parse(msg);
      const timestamp = getOrCreateMessageDatetime(hl7Message);
`````

</details>

<!-- suggestion_end -->

---

33-34: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**`filePath` is an empty string – script will read the repo root**  

`getFileNames()` will start scanning from `""` (the current working directory).  
•  On large repos this is unexpectedly slow and pulls in unrelated files.  
•  It also breaks the “place input files in *input* folder” contract mentioned in the header comment.

```diff
-const filePath = "";
+// Absolute or relative path to the folder that contains the .hl7 files
+const filePath = "./input";
```

---

#!/bin/bash
# Locate the file and print header and lines around filePath
FILE="packages/utils/src/hl7v2-notifications/convert-adt-to-fhir-local-example.ts"
echo "=== File path check ==="
if [ -f "$FILE" ]; then
  echo "Found $FILE"
else
  echo "File not found: $FILE"
  exit 1
fi
echo
echo "=== Header (lines 1–30) ==="
sed -n '1,30p' "$FILE"
echo
echo "=== Around filePath (lines 25–45) ==="
sed -n '25,45p' "$FILE"

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify header comments and default filePath in the example script
FILE="packages/utils/src/hl7v2-notifications/convert-adt-to-fhir-local-example.ts"
if [ ! -f "$FILE" ]; then
  echo "Error: File not found: $FILE" >&2
  exit 1
fi

echo "=== Header (lines 1–30) ==="
sed -n '1,30p' "$FILE"

echo
echo "=== Around filePath (lines 25–45) ==="
sed -n '25,45p' "$FILE"
```

Length of output: 2640

---



</details>

**Specify a dedicated input folder for HL7 files**  

The example script currently uses an empty `filePath` (i.e. `""`), so `getFileNames()` will scan the entire repo root—leading to slow, noisy scans and breaking the “place input files in *input* folder” contract in the header.

• Change `filePath` to point at `./input` so it only processes files in that directory.  
• Update the inline comment to clarify its purpose.

```diff
--- packages/utils/src/hl7v2-notifications/convert-adt-to-fhir-local-example.ts
@@
-// Path to the folder to scan for .hl7 files (defaults to repo root)
-const filePath = "";
+// Path to the folder that contains your .hl7 messages
+const filePath = "./input";
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
// Path to the folder that contains your .hl7 messages
const filePath = "./input";
const getDirPath = buildGetDirPathInside("hl7v2-conversion");
`````

</details>

<!-- suggestion_end -->

</blockquote></details>
<details>
<summary>packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts (1)</summary><blockquote>

164-166: _⚠️ Potential issue_

**File key ends with `.hl7.json` – likely unintended double extension**  

`buildHl7MessageFileKey()` already appends `.hl7`.  
Concatenating `JSON_FILE_EXTENSION` produces keys like  
`..._A01.hl7.json`, which is confusing and may break downstream tooling that expects a single extension.

```diff
-export function buildHl7MessageFhirBundleFileKey(params: Hl7FileKeyParams) {
-  return `${buildHl7MessageFileKey(params)}${JSON_FILE_EXTENSION}`;
+export function buildHl7MessageFhirBundleFileKey(params: Hl7FileKeyParams) {
+  return buildHl7MessageFileKey(params).replace(
+    HL7_FILE_EXTENSION,
+    JSON_FILE_EXTENSION,
+  );
 }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
export function buildHl7MessageFhirBundleFileKey(params: Hl7FileKeyParams) {
  return buildHl7MessageFileKey(params).replace(
    HL7_FILE_EXTENSION,
    JSON_FILE_EXTENSION,
  );
}
`````

</details>

<!-- suggestion_end -->

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T02:02:52Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:32:16Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:41:44Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:45:09Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:46:40Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:48:43Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:54:25Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T17:09:52Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:56:20Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T01:09:48Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T01:11:08Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T01:12:02Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T01:12:50Z
user: login: RamilGaripov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T01:38:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts (1)</summary><blockquote>

`61-66`: **Creating a new instance per message is inefficient but follows existing pattern**

Creating a new `Hl7NotificationWebhookSenderDirect` instance for each message is somewhat inefficient, but aligns with the previous review comment noting "the pattern is usually to build this where it is being invoked".



Consider enhancing error handling by adding the message details to the error object to provide more context:

```diff
      });
    } catch (err) {
      errors.push({
        err,
        msg: errorToString(err),
        timestamp,
+       messageDetails: {
+         cxId: getCxIdAndPatientIdOrFail(hl7Message)?.cxId,
+         patientId: getCxIdAndPatientIdOrFail(hl7Message)?.patientId,
+       }
      });
    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts (1)</summary><blockquote>

`34-65`: **Consider breaking down the execute method into smaller functions**

The `execute` method is quite long and performs multiple distinct operations. Consider breaking it down into smaller, more focused methods to improve readability and maintainability.


You could extract functions for:
1. Message validation and filtering
2. Patient data retrieval
3. HL7 to FHIR conversion
4. S3 upload preparation

Example refactoring:

```diff
  async execute(params: Hl7Notification): Promise<void> {
    const message = Hl7Message.parse(params.message);
    const { cxId, patientId, messageReceivedTimestamp } = params;
    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);

+   if (!this.isMessageTypeSupported(message, log)) return;
+   
+   const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+   const fhirPatient = await this.retrievePatientData(cxId, patientId);
+   const bundle = this.convertMessageToFhirBundle(message, cxId, patientId, messageReceivedTimestamp, fhirPatient);
+   log(`Conversion complete and patient entry added`);
+   
+   const newBundleFileName = this.uploadBundleToS3(bundle, {...params, messageCode, triggerEvent}, log);
+   const bundlePresignedUrl = await this.generatePresignedUrl(newBundleFileName);
+   
+   await this.notifyInternalApi(cxId, patientId, triggerEvent, bundlePresignedUrl);
+   log(`Done. API notified...`);
  }
  
+ private isMessageTypeSupported(message: Hl7Message, log: (...args: any[]) => void): boolean {
+   const { triggerEvent } = getHl7MessageTypeOrFail(message);
+   if (!supportedTypes.includes(triggerEvent)) {
+     log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+     return false;
+   }
+   return true;
+ }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between be42d2d3cb3ca346dae20ca6944e82ce9b4851b8 and b2d345d9ea0abc082423bc119e11da611de3963c.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts` (1 hunks)
* `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts` (1 hunks)
* `packages/lambdas/src/hl7-notification-webhook-sender.ts` (2 hunks)
* `packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/lambdas/src/hl7-notification-webhook-sender.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.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...</summary>

> `**/*.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/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts`
- `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts`
- `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts (2)</summary><blockquote>

<details>
<summary>packages/core/src/util/config.ts (1)</summary>

* `Config` (9-195)

</details>
<details>
<summary>packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts (1)</summary>

* `Hl7NotificationWebhookSenderDirect` (25-117)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts (1)</summary>

`7-12`: **Good improvement on dependency injection**

The updated factory now properly initializes the `Hl7NotificationWebhookSenderDirect` with required parameters in development mode. This follows good dependency injection practices by providing dependencies at creation time rather than relying on the class to fetch them internally.

</details>
<details>
<summary>packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts (1)</summary>

`5-6`: **LGTM - Imports aligned with implementation changes**

The import changes correctly support the updated implementation approach.

</details>
<details>
<summary>packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts (6)</summary>

`1-24`: **Good organization of imports and constants**

The imports are well-organized, and the new constants are defined at the top level with meaningful names. The `SIGNED_URL_DURATION_SECONDS` uses `dayjs.duration` properly as specified in the coding guidelines.

---

`25-32`: **Good use of dependency injection in constructor**

The constructor now properly accepts required dependencies (`apiUrl` and `bucketName`) with an optional `s3Utils` parameter, following good dependency injection practices. This makes the class more testable and flexible.

---

`66-81`: **Good error handling configuration for S3 uploads**

The upload parameters provide comprehensive error configuration with context, relevant parameters for debugging, and appropriate error message. This follows best practices for error handling as specified in the coding guidelines.

---

`82-103`: **Good S3 upload implementation with proper file key generation and logging**

The S3 upload implementation uses the recommended patterns for file key generation, retry logic, and appropriate logging. The presigned URL duration is set to a reasonable time (10 minutes).

---

`105-116`: **Good use of network retries for API notification**

The implementation properly uses `executeWithNetworkRetries` for the API notification, following the pattern established elsewhere in the codebase. The API call is structured correctly with parameters passed via query params.

---

`119-129`: **Well-implemented helper function for bundle manipulation**

The `prependPatientToBundle` function is concise, follows functional programming principles, and handles the edge case where the bundle has no entries. This follows the coding guideline preference for small, deterministic functions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T01:40:16Z
user: login: lucasdellabella
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T02:12:24Z
user: login: lucasdellabella
body: lgtm!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T02:12:28Z

Review comments:
diff_hunk: @@ -154,9 +153,8 @@ export function buildHl7MessageFileKey({
   timestamp,
   messageCode,
   triggerEvent,
-  extension,
 }: Hl7FileKeyParams) {
-  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}.${extension}`;
+  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}${HL7_FILE_EXTENSION}`;
path: packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
user: login: RamilGaripov
body: removing the ext. Let's follow the existing pattern, where the conversion result uses the full file name and then adds .json to it. So the hl7 message will always have the .hl7 extension, and the conversion result (file name built by buildHl7MessageFhirBundleFileKey) will have the .hl7.json
created_at: 2025-04-24T02:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
+    const { cxId, patientId, messageReceivedTimestamp, apiUrl, bucketName } = params;
+    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);
+
+    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(hl7Message);
+    if (!supportedTypes.includes(triggerEvent)) {
+      log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+      return;
+    }
+
+    const baseUrl = apiUrl || Config.getApiLoadBalancerAddress();
+    const s3BucketName = bucketName || Config.getHl7OutgoingMessageBucketName();
+    const internalHl7RouteUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
+    const internalGetPatientUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
+
+    const patient = await executeWithNetworkRetries(
+      async () => await axios.get(internalGetPatientUrl)
+    );
+    const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
 
-    const fileKey = buildHl7MessageFileKey({
+    const conversionResult = convertHl7v2MessageToFhir({
+      message: hl7Message,
       cxId,
       patientId,
-      timestamp: messageReceivedTimestamp,
-      messageId: getMessageUniqueIdentifier(message),
-      messageCode,
-      triggerEvent,
-      extension: "json",
+      timestampString: messageReceivedTimestamp,
     });
 
-    const convertedMessage = convertHl7v2MessageToFhir({
+    const bundle = saturateConvertedBundle({
+      bundle: conversionResult,
+      fhirPatient,
+    });
+    log(`Conversion complete and patient entry added`);
+
+    const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
+      s3Utils: this.s3Utils,
+      bucketName: s3BucketName,
+      contentType: JSON_APP_MIME_TYPE,
+      log,
+      errorConfig: {
+        errorMessage: "Error uploading HL7 FHIR bundle to S3",
+        context: this.context,
+        captureParams: {
+          patientId,
+          cxId,
+          messageReceivedTimestamp,
+        },
+        shouldCapture: true,
+      },
+    };
+
+    const newBundleFileName = buildHl7MessageFhirBundleFileKey({
       cxId,
       patientId,
-      message,
-      timestampString: messageReceivedTimestamp,
+      timestamp: messageReceivedTimestamp,
+      messageId: getMessageUniqueIdentifier(hl7Message),
+      messageCode: messageCode,
+      triggerEvent: triggerEvent,
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: lucasdellabella
body: can simplify
created_at: 2025-04-24T16:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,22 +48,22 @@ const filePath = "";
 const fileName = "";
 
 function invokeLambdaLogic() {
+  const hl7WebhookLambda = buildHl7NotificationWebhookSender();
path: packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts
user: login: lucasdellabella
body: the pattern is usually to build this where it is being invoked. Recomputing is not an issue, the constructor is not doing anything of substance.
created_at: 2025-04-24T16:41:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,7 +33,7 @@ export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent): Pro
     context: "hl7-notification-webhook-sender-cloud.execute",
   });
 
-  await new Hl7NotificationWebhookSenderDirect().execute(parsedBody);
+  await new Hl7NotificationWebhookSenderDirect().execute({ ...parsedBody, apiUrl, bucketName });
path: packages/lambdas/src/hl7-notification-webhook-sender.ts
user: login: lucasdellabella
body: Things that were formerly environment variables should get passed to the constructor, rather than the execute payload. 

https://github.com/metriport/metriport/blob/74e1748998bc18649bfd3637ff453fbd48a4b155/packages/lambdas/src/patient-import-create.ts#L68
created_at: 2025-04-24T16:45:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +107,7 @@ export class Hl7NotificationWebhookSenderNestedStack extends NestedStack {
       envVars: {
         // API_URL set on the api-stack after the OSS API is created
         HL7_OUTGOING_MESSAGE_BUCKET_NAME: outgoingHl7NotificationBucket.bucketName,
+        API_URL: loadBalancerDnsName,
path: packages/infra/lib/hl7-notification-webhook-sender-nested-stack.ts
user: login: lucasdellabella
body: see the comment 2 lines above - current pattern is to set the API_URL environment var in OSS. I don't know why, but that's what every one of our other lambdas does and I think it's better to not go against the grain when its a clearly established pattern.
created_at: 2025-04-24T16:46:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,9 +153,8 @@ export function buildHl7MessageFileKey({
   timestamp,
   messageCode,
   triggerEvent,
-  extension,
 }: Hl7FileKeyParams) {
-  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}.${extension}`;
+  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}${HL7_FILE_EXTENSION}`;
path: packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
user: login: lucasdellabella
body: thats pretty janky and confusing is that actually an intentional pattern, or just the result of a decision or two made a few years back?
created_at: 2025-04-24T16:48:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: lucasdellabella
body: lets call this message, you can always see what it is by hovering and checking type
created_at: 2025-04-24T16:54:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
+    const { cxId, patientId, messageReceivedTimestamp, apiUrl, bucketName } = params;
+    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);
+
+    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(hl7Message);
+    if (!supportedTypes.includes(triggerEvent)) {
+      log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+      return;
+    }
+
+    const baseUrl = apiUrl || Config.getApiLoadBalancerAddress();
+    const s3BucketName = bucketName || Config.getHl7OutgoingMessageBucketName();
+    const internalHl7RouteUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
+    const internalGetPatientUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
+
+    const patient = await executeWithNetworkRetries(
+      async () => await axios.get(internalGetPatientUrl)
+    );
+    const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
 
-    const fileKey = buildHl7MessageFileKey({
+    const conversionResult = convertHl7v2MessageToFhir({
+      message: hl7Message,
       cxId,
       patientId,
-      timestamp: messageReceivedTimestamp,
-      messageId: getMessageUniqueIdentifier(message),
-      messageCode,
-      triggerEvent,
-      extension: "json",
+      timestampString: messageReceivedTimestamp,
     });
 
-    const convertedMessage = convertHl7v2MessageToFhir({
+    const bundle = saturateConvertedBundle({
+      bundle: conversionResult,
+      fhirPatient,
+    });
+    log(`Conversion complete and patient entry added`);
+
+    const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
+      s3Utils: this.s3Utils,
+      bucketName: s3BucketName,
+      contentType: JSON_APP_MIME_TYPE,
+      log,
+      errorConfig: {
+        errorMessage: "Error uploading HL7 FHIR bundle to S3",
+        context: this.context,
+        captureParams: {
+          patientId,
+          cxId,
+          messageReceivedTimestamp,
+        },
+        shouldCapture: true,
+      },
+    };
+
+    const newBundleFileName = buildHl7MessageFhirBundleFileKey({
       cxId,
       patientId,
-      message,
-      timestampString: messageReceivedTimestamp,
+      timestamp: messageReceivedTimestamp,
+      messageId: getMessageUniqueIdentifier(hl7Message),
+      messageCode: messageCode,
+      triggerEvent: triggerEvent,
     });
 
-    this.log(`${messageReceivedTimestamp} - cx: ${cxId} - pt: ${patientId} - Uploading file to S3`);
+    await storeInS3WithRetries({
+      ...nonSpecificUploadParams,
+      payload: JSON.stringify(bundle),
+      fileName: newBundleFileName,
+    });
+    log(`Uploaded to S3. Filepath: ${newBundleFileName}`);
 
-    await this.s3Utils.uploadFile({
-      bucket: this.bucketName,
-      key: fileKey,
-      file: Buffer.from(JSON.stringify(convertedMessage)),
-      contentType: "application/json",
+    const bundlePresignedUrl = await this.s3Utils.getSignedUrl({
+      bucketName: s3BucketName,
+      fileName: newBundleFileName,
+      durationSeconds: SIGNED_URL_DURATION_SECONDS,
     });
 
-    const api = axios.create({ baseURL: Config.getApiUrl() });
-    this.log(
-      `[${messageReceivedTimestamp}] Sending webhook for cxId ${cxId} + patientId ${patientId}`
+    await executeWithNetworkRetries(
+      async () =>
+        await axios.post(internalHl7RouteUrl, undefined, {
+          params: {
+            cxId,
+            triggerEvent,
+            presignedUrl: bundlePresignedUrl,
+          },
+        })
     );
-    const response = await api.get("/");
-    this.log(`[${messageReceivedTimestamp}] response: ${JSON.stringify(response.data)}`);
+    log(`Done. API notified...`);
   }
 }
+
+function saturateConvertedBundle({
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: lucasdellabella
body: can we call this prependPatientToBundle() or something similar? Know I already signed off on this name - now that stuff has been simplified i see whats going on a bit more!
created_at: 2025-04-24T17:09:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
+    const { cxId, patientId, messageReceivedTimestamp, apiUrl, bucketName } = params;
+    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);
+
+    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(hl7Message);
+    if (!supportedTypes.includes(triggerEvent)) {
+      log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+      return;
+    }
+
+    const baseUrl = apiUrl || Config.getApiLoadBalancerAddress();
+    const s3BucketName = bucketName || Config.getHl7OutgoingMessageBucketName();
+    const internalHl7RouteUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
+    const internalGetPatientUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
+
+    const patient = await executeWithNetworkRetries(
+      async () => await axios.get(internalGetPatientUrl)
+    );
+    const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
 
-    const fileKey = buildHl7MessageFileKey({
+    const conversionResult = convertHl7v2MessageToFhir({
+      message: hl7Message,
       cxId,
       patientId,
-      timestamp: messageReceivedTimestamp,
-      messageId: getMessageUniqueIdentifier(message),
-      messageCode,
-      triggerEvent,
-      extension: "json",
+      timestampString: messageReceivedTimestamp,
     });
 
-    const convertedMessage = convertHl7v2MessageToFhir({
+    const bundle = saturateConvertedBundle({
+      bundle: conversionResult,
+      fhirPatient,
+    });
+    log(`Conversion complete and patient entry added`);
+
+    const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
+      s3Utils: this.s3Utils,
+      bucketName: s3BucketName,
+      contentType: JSON_APP_MIME_TYPE,
+      log,
+      errorConfig: {
+        errorMessage: "Error uploading HL7 FHIR bundle to S3",
+        context: this.context,
+        captureParams: {
+          patientId,
+          cxId,
+          messageReceivedTimestamp,
+        },
+        shouldCapture: true,
+      },
+    };
+
+    const newBundleFileName = buildHl7MessageFhirBundleFileKey({
       cxId,
       patientId,
-      message,
-      timestampString: messageReceivedTimestamp,
+      timestamp: messageReceivedTimestamp,
+      messageId: getMessageUniqueIdentifier(hl7Message),
+      messageCode: messageCode,
+      triggerEvent: triggerEvent,
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: RamilGaripov
body: done
created_at: 2025-04-25T00:56:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,7 +33,7 @@ export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent): Pro
     context: "hl7-notification-webhook-sender-cloud.execute",
   });
 
-  await new Hl7NotificationWebhookSenderDirect().execute(parsedBody);
+  await new Hl7NotificationWebhookSenderDirect().execute({ ...parsedBody, apiUrl, bucketName });
path: packages/lambdas/src/hl7-notification-webhook-sender.ts
user: login: RamilGaripov
body: great point! fixed!
created_at: 2025-04-25T01:09:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +107,7 @@ export class Hl7NotificationWebhookSenderNestedStack extends NestedStack {
       envVars: {
         // API_URL set on the api-stack after the OSS API is created
         HL7_OUTGOING_MESSAGE_BUCKET_NAME: outgoingHl7NotificationBucket.bucketName,
+        API_URL: loadBalancerDnsName,
path: packages/infra/lib/hl7-notification-webhook-sender-nested-stack.ts
user: login: RamilGaripov
body: Hm.. but what about the LB URL? Should I just set it as "API_LB_URL"? Or that's also set automatically?
created_at: 2025-04-25T01:11:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: RamilGaripov
body: sure, i dont mind that
created_at: 2025-04-25T01:12:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,130 @@
 import { Hl7Message } from "@medplum/core";
+import { Bundle, Resource } from "@medplum/fhirtypes";
+import { executeWithNetworkRetries } from "@metriport/shared";
 import axios from "axios";
-import { S3Utils } from "../../external/aws/s3";
+import dayjs from "dayjs";
+import { S3Utils, StoreInS3Params, storeInS3WithRetries } from "../../external/aws/s3";
+import { toFHIR as toFhirPatient } from "../../external/fhir/patient/conversion";
+import { buildBundle, buildBundleEntry } from "../../external/fhir/shared/bundle";
+import { out } from "../../util";
 import { Config } from "../../util/config";
+import { JSON_APP_MIME_TYPE } from "../../util/mime";
+import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion";
 import {
   getHl7MessageTypeOrFail,
   getMessageUniqueIdentifier,
 } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/msh";
-import { buildHl7MessageFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
+import { buildHl7MessageFhirBundleFileKey } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared";
 import { Hl7Notification, Hl7NotificationWebhookSender } from "./hl7-notification-webhook-sender";
-import { convertHl7v2MessageToFhir } from "../hl7v2-subscriptions/hl7v2-to-fhir-conversion/index";
-import { out } from "../../util/log";
+
+const supportedTypes = ["A01", "A03"];
+const INTERNAL_HL7_ENDPOINT = `notification`;
+const INTERNAL_PATIENT_ENDPOINT = "internal/patient";
+const SIGNED_URL_DURATION_SECONDS = dayjs.duration({ minutes: 10 }).asSeconds();
 
 export class Hl7NotificationWebhookSenderDirect implements Hl7NotificationWebhookSender {
-  private readonly context = "hl7-notification-webhook-sender";
-  private readonly log;
-  private readonly bucketName: string;
+  private readonly context = "hl7-notification-wh-sender";
   private readonly s3Utils: S3Utils;
 
   constructor() {
-    const { log } = out(this.context);
-    this.log = log;
-    this.bucketName = Config.getHl7OutgoingMessageBucketName();
     this.s3Utils = new S3Utils(Config.getAWSRegion());
   }
 
   async execute(params: Hl7Notification): Promise<void> {
-    const message = Hl7Message.parse(params.message);
-    const { cxId, patientId, messageReceivedTimestamp } = params;
-    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(message);
+    const hl7Message = Hl7Message.parse(params.message);
+    const { cxId, patientId, messageReceivedTimestamp, apiUrl, bucketName } = params;
+    const { log } = out(`${this.context}, cx: ${cxId}, pt: ${patientId}`);
+
+    const { messageCode, triggerEvent } = getHl7MessageTypeOrFail(hl7Message);
+    if (!supportedTypes.includes(triggerEvent)) {
+      log(`Trigger event ${triggerEvent} is not supported. Skipping...`);
+      return;
+    }
+
+    const baseUrl = apiUrl || Config.getApiLoadBalancerAddress();
+    const s3BucketName = bucketName || Config.getHl7OutgoingMessageBucketName();
+    const internalHl7RouteUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}/${INTERNAL_HL7_ENDPOINT}`;
+    const internalGetPatientUrl = `${baseUrl}/${INTERNAL_PATIENT_ENDPOINT}/${patientId}?cxId=${cxId}`;
+
+    const patient = await executeWithNetworkRetries(
+      async () => await axios.get(internalGetPatientUrl)
+    );
+    const fhirPatient = toFhirPatient({ id: patientId, data: patient.data });
 
-    const fileKey = buildHl7MessageFileKey({
+    const conversionResult = convertHl7v2MessageToFhir({
+      message: hl7Message,
       cxId,
       patientId,
-      timestamp: messageReceivedTimestamp,
-      messageId: getMessageUniqueIdentifier(message),
-      messageCode,
-      triggerEvent,
-      extension: "json",
+      timestampString: messageReceivedTimestamp,
     });
 
-    const convertedMessage = convertHl7v2MessageToFhir({
+    const bundle = saturateConvertedBundle({
+      bundle: conversionResult,
+      fhirPatient,
+    });
+    log(`Conversion complete and patient entry added`);
+
+    const nonSpecificUploadParams: Omit<StoreInS3Params, "fileName" | "payload"> = {
+      s3Utils: this.s3Utils,
+      bucketName: s3BucketName,
+      contentType: JSON_APP_MIME_TYPE,
+      log,
+      errorConfig: {
+        errorMessage: "Error uploading HL7 FHIR bundle to S3",
+        context: this.context,
+        captureParams: {
+          patientId,
+          cxId,
+          messageReceivedTimestamp,
+        },
+        shouldCapture: true,
+      },
+    };
+
+    const newBundleFileName = buildHl7MessageFhirBundleFileKey({
       cxId,
       patientId,
-      message,
-      timestampString: messageReceivedTimestamp,
+      timestamp: messageReceivedTimestamp,
+      messageId: getMessageUniqueIdentifier(hl7Message),
+      messageCode: messageCode,
+      triggerEvent: triggerEvent,
     });
 
-    this.log(`${messageReceivedTimestamp} - cx: ${cxId} - pt: ${patientId} - Uploading file to S3`);
+    await storeInS3WithRetries({
+      ...nonSpecificUploadParams,
+      payload: JSON.stringify(bundle),
+      fileName: newBundleFileName,
+    });
+    log(`Uploaded to S3. Filepath: ${newBundleFileName}`);
 
-    await this.s3Utils.uploadFile({
-      bucket: this.bucketName,
-      key: fileKey,
-      file: Buffer.from(JSON.stringify(convertedMessage)),
-      contentType: "application/json",
+    const bundlePresignedUrl = await this.s3Utils.getSignedUrl({
+      bucketName: s3BucketName,
+      fileName: newBundleFileName,
+      durationSeconds: SIGNED_URL_DURATION_SECONDS,
     });
 
-    const api = axios.create({ baseURL: Config.getApiUrl() });
-    this.log(
-      `[${messageReceivedTimestamp}] Sending webhook for cxId ${cxId} + patientId ${patientId}`
+    await executeWithNetworkRetries(
+      async () =>
+        await axios.post(internalHl7RouteUrl, undefined, {
+          params: {
+            cxId,
+            triggerEvent,
+            presignedUrl: bundlePresignedUrl,
+          },
+        })
     );
-    const response = await api.get("/");
-    this.log(`[${messageReceivedTimestamp}] response: ${JSON.stringify(response.data)}`);
+    log(`Done. API notified...`);
   }
 }
+
+function saturateConvertedBundle({
path: packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts
user: login: RamilGaripov
body: ya that's a better name
created_at: 2025-04-25T01:12:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +107,7 @@ export class Hl7NotificationWebhookSenderNestedStack extends NestedStack {
       envVars: {
         // API_URL set on the api-stack after the OSS API is created
         HL7_OUTGOING_MESSAGE_BUCKET_NAME: outgoingHl7NotificationBucket.bucketName,
+        API_URL: loadBalancerDnsName,
path: packages/infra/lib/hl7-notification-webhook-sender-nested-stack.ts
user: login: RamilGaripov
body: fixed
created_at: 2025-04-25T01:38:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,9 +153,8 @@ export function buildHl7MessageFileKey({
   timestamp,
   messageCode,
   triggerEvent,
-  extension,
 }: Hl7FileKeyParams) {
-  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}.${extension}`;
+  return `${cxId}/${patientId}/${timestamp}_${messageId}_${messageCode}_${triggerEvent}${HL7_FILE_EXTENSION}`;
path: packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts
user: login: lucasdellabella
body: Shared in #engineering - seems like its a pattern! :squint: sussy but.... 🚢 !
created_at: 2025-04-25T02:12:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-24T01:58:24Z
body: <p><a href="https://linear.app/metriport/issue/ENG-89/hl7-to-fhir-converter-lambda">ENG-89 HL7 to FHIR Converter Lambda</a></p>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-24T01:58:29Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes refactor how HL7 notification webhooks and HL7-to-FHIR conversion processes handle file key generation, environment configuration, and class instantiation. The `Hl7NotificationWebhookSenderDirect` class is updated to accept `apiUrl` and `bucketName` as constructor parameters and includes improved HL7 message filtering, API URL construction, patient data retrieval, FHIR conversion, S3 upload with retry logic, and webhook notification. File key generation now uses fixed constants for file extensions, eliminating the need to specify extensions in function calls. Factory and handler functions are updated to pass the required parameters. Utility constants for file extensions are introduced, and related code is updated to use these constants.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                                                                                                                                                                                            |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-direct.ts`       | Refactored `Hl7NotificationWebhookSenderDirect` to accept `apiUrl` and `bucketName` in the constructor; improved HL7 message filtering, API URL construction, patient data retrieval, FHIR conversion, S3 upload with retry logic, and webhook notification; added `prependPatientToBundle` helper function.                                                        |
| `packages/core/src/command/hl7-notification/hl7-notification-webhook-sender-factory.ts`      | Updated factory to instantiate `Hl7NotificationWebhookSenderDirect` with `ApiLoadBalancerAddress` and `Hl7OutgoingMessageBucketName` in development mode.                                                                                                                           |
| `packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts`           | Removed `extension` property from `Hl7FileKeyParams` and related function signatures; updated `buildHl7MessageFileKey` to append fixed HL7 extension constant internally.                                                                                                                                                                           |
| `packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/index.ts`            | Removed `"extension": "hl7"` property from arguments to `buildHl7MessageFileKey`.                                                                                                                                                                                                                                           |
| `packages/core/src/util/mime.ts`                                                             | Added constant `HL7_FILE_EXTENSION = "hl7"`.                                                                                                                                                                                                                                                                                                                     |
| `packages/lambdas/src/hl7-notification-webhook-sender.ts`                                    | Reordered imports; retrieves `HL7_OUTGOING_MESSAGE_BUCKET_NAME` and `API_URL` from environment; instantiates `Hl7NotificationWebhookSenderDirect` with these parameters.                                                                                                             |
| `packages/mllp-server/src/app.ts`                                                            | Removed `"extension": "hl7"` property from arguments to `buildHl7MessageFileKey`.                                                                                                                                                                                                                                           |
| `packages/utils/src/hl7v2-notifications/hl7-to-fhir-converter-logic.ts`                      | Removed usage of `getMessageUniqueIdentifier` and `convertHl7MessageToFhirAndUpload`; now instantiates and uses `Hl7NotificationWebhookSenderDirect` with `apiUrl` and `bucketName`.                                                                                                  |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant LambdaHandler
    participant Hl7NotificationWebhookSenderDirect
    participant InternalAPI
    participant S3
    participant WebhookEndpoint

    LambdaHandler->>Hl7NotificationWebhookSenderDirect: execute(Hl7Notification)
    Hl7NotificationWebhookSenderDirect->>InternalAPI: fetch patient data (with retry)
    InternalAPI-->>Hl7NotificationWebhookSenderDirect: patient data
    Hl7NotificationWebhookSenderDirect->>Hl7NotificationWebhookSenderDirect: convert HL7 to FHIR bundle, prepend patient
    Hl7NotificationWebhookSenderDirect->>S3: upload FHIR bundle (with retry)
    S3-->>Hl7NotificationWebhookSenderDirect: presigned URL
    Hl7NotificationWebhookSenderDirect->>WebhookEndpoint: POST notification with presigned URL (with retry)
    WebhookEndpoint-->>Hl7NotificationWebhookSenderDirect: acknowledgement
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b2d345d9ea0abc082423bc119e11da611de3963c and 32a71eb532a4804619e5e4adf0f834147f6543d8.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts` (2 hunks)
* `packages/core/src/util/mime.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* packages/core/src/util/mime.ts
* packages/core/src/command/hl7v2-subscriptions/hl7v2-to-fhir-conversion/shared.ts

</details>

</details>
<!-- internal state start -->


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

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=metriport/metriport&utm_content=3716):

- 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 generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

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

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE