-
Notifications
You must be signed in to change notification settings - Fork 68
feat(healthie): integration v0 #3754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
WalkthroughThis set of changes introduces comprehensive support for the "Healthie" EHR integration throughout the codebase. It adds new infrastructure components (SQS queue, Lambda), API client, data schemas, and routes for Healthie, mirroring the architecture used for existing EHR integrations. The backend now supports Healthie patient synchronization, appointment processing, webhook subscription and handling, JWT token management, and internal admin endpoints. Type definitions, validation schemas, and configuration utilities are added for Healthie-specific data and flows. The API, core logic, and infrastructure are updated to orchestrate Healthie-related operations, including background processing and secure webhook verification. Changes
Sequence Diagram(s)Healthie Patient Sync FlowsequenceDiagram
participant Client
participant API
participant HealthieApi
participant MetriportDB
Client->>API: POST /ehr/healthie/patient/:id (with cxId, practiceId)
API->>HealthieApi: getPatient({cxId, patientId})
HealthieApi-->>API: Patient data
API->>MetriportDB: Find or create Metriport patient
MetriportDB-->>API: Metriport patient ID
API->>HealthieApi: updatePatientQuickNotesWithLink({cxId, patientId, link})
API-->>Client: { patientId: ... }
Healthie Appointment Webhook FlowsequenceDiagram
participant HealthieWebhook
participant API
participant HealthieApi
participant LinkPatientHandler
HealthieWebhook->>API: POST /ehr/webhook/healthie/:practiceId/appointment/created
API->>HealthieApi: getAppointment({cxId, appointmentId})
HealthieApi-->>API: Appointment with attendee
API->>HealthieApi: updatePatientQuickNotesWithLink({cxId, patientId, link})
API->>LinkPatientHandler: processLinkPatient({cxId, practiceId, patientId})
API-->>HealthieWebhook: 200 OK
Healthie Webhook Subscription FlowsequenceDiagram
participant Admin
participant API
participant HealthieApi
participant MetriportDB
Admin->>API: POST /internal/cx-mapping (with source=healthie)
API->>HealthieApi: subscribeToResource({cxId, practiceId, resource})
HealthieApi-->>API: Subscription details (event type, url, secret)
API->>MetriportDB: Update cx-mapping secondaryMappings with webhook info
API-->>Admin: 200 OK
Healthie Background Appointment ProcessingsequenceDiagram
participant Scheduler
participant API
participant HealthieApi
participant LinkPatientHandler
Scheduler->>API: Trigger processPatientsFromAppointments
API->>HealthieApi: getAppointments({cxId, practiceId, date range})
HealthieApi-->>API: List of appointments
loop For each unique patient
API->>LinkPatientHandler: processLinkPatient({cxId, practiceId, patientId})
API->>API: syncHealthiePatientIntoMetriport(...)
end
API-->>Scheduler: Done
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
🧹 Nitpick comments (27)
packages/api/src/routes/ehr/healthie/routes/webhook.ts (1)
7-8
: Consider adding input validation middlewareWhile the implementation is clean, consider adding input validation middleware for the
:practiceId
parameter to ensure it's in the expected format before reaching the handlers.const routes = Router(); +// Middleware to validate practiceId +routes.param('practiceId', (req, res, next, practiceId) => { + // Example: Check if practiceId is a valid UUID or meets specific format requirements + if (!/^[a-zA-Z0-9-_]+$/.test(practiceId)) { + return res.status(400).json({ error: 'Invalid practiceId format' }); + } + next(); +}); + routes.use("/:practiceId/appointment", appointmentWebhook); routes.use("/:practiceId/patient", patientWebhook);packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (1)
6-13
: Function looks good, but consider error handling for the queue URL retrieval.The factory function properly implements environment-specific handlers, following the factory pattern well. In non-development environments,
Config.getHealthieLinkPatientQueueUrl()
might throw if the environment variable is missing, which would be uncaught in the current implementation.Consider adding error handling for the case where the queue URL cannot be retrieved in non-development environm 8000 ents:
if (Config.isDev()) { const waitTimeAtTheEndInMillis = 0; return new HealthieLinkPatientLocal(waitTimeAtTheEndInMillis); } - const healthieLinkPatientQueueUrl = Config.getHealthieLinkPatientQueueUrl(); - return new HealthieLinkPatientCloud(healthieLinkPatientQueueUrl); + try { + const healthieLinkPatientQueueUrl = Config.getHealthieLinkPatientQueueUrl(); + return new HealthieLinkPatientCloud(healthieLinkPatientQueueUrl); + } catch (error) { + throw new Error(`Failed to create Healthie link patient handler: ${error.message}`); + }packages/api/src/routes/internal/ehr/healthie/secret-key.ts (1)
26-28
: Consider adding more specific error message.The error handling for invalid resources is good, but consider making the error message more specific to guide API consumers.
if (!isSubscriptionResource(resource)) { - throw new BadRequestError("Invalid resource", undefined, { resource }); + throw new BadRequestError( + `Invalid subscription resource: '${resource}'`, + undefined, + { resource, validResources: "<list of valid resources>" } + ); }packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
6-22
: Solid implementation with effective parameter handling.The class correctly implements the
HealthieLinkPatientHandler
interface and properly uses the linkPatient function with appropriate parameters. The conditional wait time is a good feature for testing/development.Consider adding logging to help with debugging and monitoring, especially around the linkPatient call:
async processLinkPatient({ cxId, practiceId, patientId, }: ProcessLinkPatientRequest): Promise<void> { + const { log } = out(`Healthie processLinkPatient - cxId ${cxId}`); + log(`Processing link patient request for practiceId: ${practiceId}, patientId: ${patientId}`); await linkPatient({ ehr: EhrSources.healthie, cxId, practiceId, patientId, }); + log(`Successfully processed link patient request`); if (this.waitTimeInMillis > 0) await sleep(this.waitTimeInMillis); }packages/api/src/routes/ehr/healthie/patient.ts (2)
13-37
: Refactor to eliminate duplication between GET and POST handlers.The GET and POST routes have identical implementation which creates unnecessary code duplication. While supporting both HTTP methods might be intentional, the duplicated code should be extracted into a shared function.
// Add a shared handler function +const handlePatientSync = async (req: Request, res: Response): Promise<Response> => { + const cxId = getCxIdOrFail(req); + const healthiePatientId = getFrom("params").orFail("id", req); + const healthiePracticeId = getFromQueryOrFail("practiceId", req); + const patientId = await syncHealthiePatientIntoMetriport({ + cxId, + healthiePracticeId, + healthiePatientId, + }); + return res.status(httpStatus.OK).json(patientId); +}; // Use the shared handler in both routes -router.get( - "/:id", - handleParams, - processEhrPatientId(tokenEhrPatientIdQueryParam, "params"), - requestLogger, - asyncHandler(async (req: Request, res: Response) => { - const cxId = getCxIdOrFail(req); - const healthiePatientId = getFrom("params").orFail("id", req); - const healthiePracticeId = getFromQueryOrFail("practiceId", req); - const patientId = await syncHealthiePatientIntoMetriport({ - cxId, - healthiePracticeId, - healthiePatientId, - }); - return res.status(httpStatus.OK).json(patientId); - }) -); +router.get( + "/:id", + handleParams, + processEhrPatientId(tokenEhrPatientIdQueryParam, "params"), + requestLogger, + asyncHandler(handlePatientSync) +); -router.post( - "/:id", - handleParams, - processEhrPatientId(tokenEhrPatientIdQueryParam, "params"), - requestLogger, - asyncHandler(async (req: Request, res: Response) => { - const cxId = getCxIdOrFail(req); - const healthiePatientId = getFrom("params").orFail("id", req); - const healthiePracticeId = getFromQueryOrFail("practiceId", req); - const patientId = await syncHealthiePatientIntoMetriport({ - cxId, - healthiePracticeId, - healthiePatientId, - }); - return res.status(httpStatus.OK).json(patientId); - }) -); +router.post( + "/:id", + handleParams, + processEhrPatientId(tokenEhrPatientIdQueryParam, "params"), + requestLogger, + asyncHandler(handlePatientSync) +);Also applies to: 39-63
21-37
: Consider adding informative logging.The current implementation has no custom logging in the route handlers. Adding logging would help trace API requests and monitor synchronization operations.
asyncHandler(async (req: Request, res: Response) => { const cxId = getCxIdOrFail(req); const healthiePatientId = getFrom("params").orFail("id", req); const healthiePracticeId = getFromQueryOrFail("practiceId", req); + console.log(`Syncing Healthie patient ${healthiePatientId} from practice ${healthiePracticeId} for customer ${cxId}`); const patientId = await syncHealthiePatientIntoMetriport({ cxId, healthiePracticeId, healthiePatientId, }); + console.log(`Successfully synced Healthie patient ${healthiePatientId} to Metriport patient ${patientId}`); return res.status(httpStatus.OK).json(patientId); })Also applies to: 47-63
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
1-26
: Add logging for traceabilityConsider adding logging to capture the start and completion of the operation for better traceability.
+import { out } from "../../../../util/log"; import { createUuidFromText } from "@metriport/shared/common/uuid"; import { Config } from "../../../../util/config"; import { SQSClient } from "../../../aws/sqs"; import { HealthieLinkPatientHandler, ProcessLinkPatientRequest } from "./healthie-link-patient"; export class HealthieLinkPatientCloud implements HealthieLinkPatientHandler { private readonly sqsClient: SQSClient; constructor( private readonly healthieLinkPatientQueueUrl: string, region?: string, sqsClient?: SQSClient ) { this.sqsClient = sqsClient ?? new SQSClient({ region: region ?? Config.getAWSRegion() }); } async processLinkPatient(params: ProcessLinkPatientRequest): Promise<void> { + const { log } = out(`Healthie.LinkPatient.Cloud - cxId ${params.cxId}`); const { cxId } = params; const payload = JSON.stringify(params); + log(`Enqueueing link patient request`); await this.sqsClient.sendMessageToQueue(this.healthieLinkPatientQueueUrl, payload, { fifo: true, messageDeduplicationId: createUuidFromText(payload), messageGroupId: cxId, }); + log(`Successfully enqueued link patient request`); } }packages/core/src/external/ehr/api/link-patient.ts (1)
31-35
: Consider adding validation for the API responseWhile the function checks if
response.data
exists, it might be worth adding more specific validation of the response structure based on the expected format from different EHR sources.try { const response = await api.post(linkPatientUrl); - if (!response.data) throw new Error(`No body returned from ${linkPatientUrl}`); + if (!response.data) { + throw new Error(`No body returned from ${linkPatientUrl} for ehr: ${ehr}`); + } + if (response.status !== 200) { + throw new Error(`Unexpected status code: ${response.status} from ${linkPatientUrl} for ehr: ${ehr}`); + } debug(`${linkPatientUrl} resp: ${JSON.stringify(response.data)}`); } catch (error) {packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)
1-10
: Consider adding additional token data validationJWT tokens often include standard claims like
exp
(expiration time),iat
(issued at time), orsub
(subject). Consider extending the schema to include these standard JWT claims for more comprehensive validation.import z from "zod"; import { EhrSources } from "../source"; export const healthieDashSource = EhrSources.healthie as const; export const healthieDashJwtTokenDataSchema = z.object({ practiceId: z.string(), patientId: z.string(), source: z.literal(`${healthieDashSource}`), + exp: z.number().optional(), // Expiration time + iat: z.number().optional(), // Issued at time + sub: z.string().optional(), // Subject (typically user ID) }); export type HealthieDashJwtTokenData = z.infer<typeof healthieDashJwtTokenDataSchema>;packages/api/src/routes/internal/jwt-token/healthie.ts (1)
1-54
: Healthie JWT token routes look good with minor suggestions.The implementation provides clean endpoints for checking and saving JWT tokens specifically for Healthie integration.
I suggest a couple of improvements:
- Consider extracting the
createJwtSchema
to a shared location if it's used in multiple places in the codebase- Adding some basic validation for the JWT token format before passing it to the token operations
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)
9-19
: Consider adding JSDoc comments for feature flags.The secondary mappings schema includes multiple boolean flags controlling different features. While the names are descriptive, adding JSDoc comments would improve maintainability by explaining the purpose and implications of each flag.
For example:
+ /** + * When enabled, disables patient linking when receiving patient webhooks + */ webhookPatientPatientLinkingDisabled: z.boolean().optional(),
9-19
: Consider grouping related flags into sub-objects.There are several related boolean flags that could be organized into logical groupings to improve readability and maintainability:
- webhookPatientPatientLinkingDisabled: z.boolean().optional(), - webhookPatientPatientProcessingEnabled: z.boolean().optional(), - webhookAppointmentPatientLinkingDisabled: z.boolean().optional(), - webhookAppointmentPatientProcessingDisabled: z.boolean().optional(), + webhookPatient: z.object({ + patientLinkingDisabled: z.boolean().optional(), + patientProcessingEnabled: z.boolean().optional(), + }).optional(), + webhookAppointment: z.object({ + patientLinkingDisabled: z.boolean().optional(), + patientProcessingDisabled: z.boolean().optional(), + }).optional(),This would make the configuration more hierarchical and self-documenting.
packages/api/src/routes/internal/ehr/healthie/patient.ts (4)
25-27
: Fire-and-forget pattern should add more context to error messages.The code uses the fire-and-forget pattern (not awaiting the promise) with error handling via
.catch()
. This is acceptable, but consider enhancing the error context:processPatientsFromAppointments({ lookupMode: LookupModes.Appointments }).catch( - processAsyncError("Healthie processPatientsFromAppointments") + processAsyncError(`Healthie processPatientsFromAppointments - lookupMode: ${LookupModes.Appointments}`) );This would make debugging easier by providing more specific context in error logs.
60-74
: Consider using POST body instead of query parameters.This endpoint uses query parameters for data that modifies resources. Following RESTful principles, consider using the request body for this data:
asyncHandler(async (req: Request, res: Response) => { - const cxId = getUUIDFrom("query", req, "cxId").orFail(); - const healthiePatientId = getFromQueryOrFail("patientId", req); - const healthiePracticeId = getFromQueryOrFail("practiceId", req); - const triggerDq = getFromQueryAsBoolean("triggerDq", req); + const { cxId, patientId: healthiePatientId, practiceId: healthiePracticeId, triggerDq = false } = req.body; + + // Validate required fields + if (!cxId || !healthiePatientId || !healthiePracticeId) { + throw new BadRequestError("Missing required fields: cxId, patientId, practiceId"); + } syncHealthiePatientIntoMetriport({ cxId, healthiePracticeId, healthiePatientId, triggerDq, }).catch(processAsyncError("Healthie syncHealthiePatientIntoMetriport")); return res.sendStatus(httpStatus.OK); })Additionally, using a request body schema with zod would provide better validation.
67-73
: Add more context to the error logs for better debugging.Enhance the error context with patient and practice information:
syncHealthiePatientIntoMetriport({ cxId, healthiePracticeId, healthiePatientId, triggerDq, }).catch( - processAsyncError("Healthie syncHealthiePatientIntoMetriport") + processAsyncError(`Healthie syncHealthiePatientIntoMetriport for cxId: ${cxId}, patientId: ${healthiePatientId}`) );
87-99
: Apply consistent pattern for request data handling.For consistency with the previous suggestion, consider using request body instead of query parameters here as well:
asyncHandler(async (req: Request, res: Response) => { - const cxId = getUUIDFrom("query", req, "cxId").orFail(); - const healthiePatientId = getFromQueryOrFail("patientId", req); - const healthiePracticeId = getFromQueryOrFail("practiceId", req); + const { cxId, patientId: healthiePatientId, practiceId: healthiePracticeId } = req.body; + + // Validate required fields + if (!cxId || !healthiePatientId || !healthiePracticeId) { + throw new BadRequestError("Missing required fields: cxId, patientId, practiceId"); + } updateHealthiePatientQuickNotes({ cxId, healthiePracticeId, healthiePatientId, }).catch(processAsyncError(`Healthie updateHealthiePatientQuickNotes for cxId: ${cxId}, patientId: ${healthiePatientId}`)); return res.sendStatus(httpStatus.OK); })packages/shared/src/interface/external/ehr/healthie/event.ts (2)
3-8
: Consider adding additional validation for event data.The current schema correctly defines the basic structure for appointment created events, but consider enhancing validation:
- If
resource_id
is expected to be a UUID or specific format, add pattern validation- If
resource_id_type
has a limited set of valid values, use an enum- Consider adding documentation comments explaining these fields
export const healthieAppointmentCreatedEventSchema = z.object({ - resource_id: z.coerce.string(), + resource_id: z.coerce.string().min(1, "Resource ID cannot be empty"), - resource_id_type: z.string(), + resource_id_type: z.enum(["appointment_id", "external_id"], { + errorMap: () => ({ message: "resource_id_type must be 'appointment_id' or 'external_id'" }), + }), event_type: z.literal("appointment.created"), });
10-15
: Apply consistent validation patterns.For consistency, apply the same validation enhancements to the patient created event schema:
export const healthiePatientCreatedEventSchema = z.object({ - resource_id: z.coerce.string(), + resource_id: z.coerce.string().min(1, "Resource ID cannot be empty"), - resource_id_type: z.string(), + resource_id_type: z.enum(["patient_id", "external_id"], { + errorMap: () => ({ message: "resource_id_type must be 'patient_id' or 'external_id'" }), + }), event_type: z.literal("patient.created"), });Also consider creating a union type for when you need to handle multiple event types:
export type HealthieEvent = HealthieAppointmentCreatedEvent | HealthiePatientCreatedEvent;packages/core/src/util/webhook.ts (1)
58-59
: Consider using a more specific type thanany
.While the ESLint disable comment acknowledges the use of
any
, consider using a more specific type for better type safety, such asunknown
or a union type of expected payload structures.- body: any; // eslint-disable-line @typescript-eslint/no-explicit-any + body: unknown; // More type-safe than anypackages/api/src/routes/ehr/healthie/patient-webhook.ts (1)
35-40
: Consider extracting the error message to a constant.Since the same error might be thrown in multiple places (e.g., in the appointment-webhook.ts file as well), consider extracting the error message to a shared constant.
+ const HEALTHIE_MAPPINGS_NOT_FOUND_ERROR = "Healthie secondary mappings not found"; // Later in code - throw new MetriportError("Healthie secondary mappings not found", undefined, { + throw new MetriportError(HEALTHIE_MAPPINGS_NOT_FOUND_ERROR, undefined, { externalId: healthiePracticeId, source: EhrSources.healthie, });packages/infra/lib/ehr-nested-stack.ts (1)
378-425
: Consider refactoring setup methods to reduce duplication.There's significant code duplication between
setupHealthieLinkPatient
,setupElationLinkPatient
, and other setup methods. Consider extracting common functionality to a shared method that can be parameterized.+ private setupEhrLinkPatient(ehrName: string, settings: QueueAndLambdaSettings, ownProps: { + lambdaLayers: LambdaLayers; + vpc: ec2.IVpc; + envType: EnvType; + sentryDsn: string | undefined; + alarmAction: SnsAction | undefined; + }): { lambda: Lambda; queue: Queue } { + const { lambdaLayers, vpc, envType, sentryDsn, alarmAction } = ownProps; + const { name, entry, lambda: lambdaSettings, queue: queueSettings, + eventSource: eventSourceSettings, waitTime } = settings; + + const queue = createQueue({ + ...queueSettings, + stack: this, + name, + fifo: true, + createDLQ: true, + lambdaLayers: [lambdaLayers.shared], + envType, + alarmSnsAction: alarmAction, + }); + + const lambda = createLambda({ + ...lambdaSettings, + stack: this, + name, + entry, + envType, + envVars: { + // API_URL set on the api-stack after the OSS API is created + WAIT_TIME_IN_MILLIS: waitTime.toMilliseconds().toString(), + ...(sentryDsn ? { SENTRY_DSN: sentryDsn } : {}), + }, + layers: [lambdaLayers.shared], + vpc, + alarmSnsAction: alarmAction, + }); + + lambda.addEventSource(new SqsEventSource(queue, eventSourceSettings)); + + return { lambda, queue }; + } // Then in the specific setup methods: private setupHealthieLinkPatient(ownProps: {...}): { lambda: Lambda; queue: Queue } { return this.setupEhrLinkPatient("healthie", settings().healthieLinkPatient, ownProps); }packages/api/src/external/ehr/shared.ts (1)
159-177
:getLookForwardTimeRangeWithOffset
– possible off-by-logic & missing documentationThe function returns
startRange = now + offset
,
endRange = now + lookForward
.If
offset > lookForward
we throw (good), but for the common case the window length shrinks tolookForward - offset
instead of the fulllookForward
.
If the intention was a fulllookForward
window starting atoffset
,endRange
should benow + offset + lookForward
.At minimum please add a JSDoc comment explaining the expected semantics and confirm the current behaviour is desired.
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
10-18
: Tighten the schema: constrainevent_type
to known values
event_type
is currentlyz.string().nullable()
, which silently allows typos or future, unsupported values.
Restricting it to theSubscriptionResource
union gives us compile-time and run-time safety.- event_type: z.string().nullable(), + event_type: z.enum(subscriptionResources).nullable(),Apply the same change to
subscriptionSchema
(line 16) andwebhookEventSchema
(line 11) for consistency.packages/core/src/external/ehr/healthie/index.ts (1)
51-56
: Redundant Axios instance creation
axiosInstance
is created twice—once in the constructor (line 54) and again ininitialize()
(line 77).
The first instance is thrown away, wasting a tiny bit of memory and making the constructor do perf-irrelevant work that could hide future bugs.- this.axiosInstance = axios.create({}); this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`;
packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)
30-37
: Sanitise header types before signature verification
req.headers
can containstring | string[] | undefined
values, butverifySignature
expects a flatRecord<string,string>
.
Casting hides a potential runtime error where an array sneaks through and.split("=")
(insideverifySignature
) blows up.- headers: req.headers as Record<string, string>, + headers: Object.fromEntries( + Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v ?? ""]) + ),packages/shared/src/interface/external/ehr/healthie/patient.ts (1)
3-10
: Make address line 2 & country fields optionalReal-world data often omits
line2
and sometimescountry
. Marking them as required will cause valid payloads to fail schema validation and abort ingest.- line2: z.string(), + line2: z.string().nullable().optional(), - country: z.string(), + country: z.string().nullable().optional(),Consider also making the other address fields nullable/optional if Healthie allows them to be blank.
packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
193-199
: Rename local variable for clarity
ehrDashUrl
actually holds the final patient link, not the dash base URL, which can mislead future readers.- const ehrDashUrl = await createHealthiePatientLink({ healthiePracticeId, healthiePatientId }); + const patientLink = await createHealthiePatientLink({ healthiePracticeId, healthiePatientId }); const api = healthieApi ?? (await createHealthieClient({ cxId, practiceId: healthiePracticeId })); await api.updatePatientQuickNotesWithLink({ cxId, patientId: healthiePatientId, - link: ehrDashUrl, + link: patientLink, });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (54)
packages/api/src/external/ehr/elation/command/sync-patient.ts
(2 hunks)packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/sync-patient.ts
(1 hunks)packages/api/src/external/ehr/healthie/shared.ts
(1 hunks)packages/api/src/external/ehr/shared.ts
(8 hunks)packages/api/src/routes/ehr/athenahealth/patient.ts
(2 hunks)packages/api/src/routes/ehr/elation/auth/middleware.ts
(1 hunks)packages/api/src/routes/ehr/elation/patient.ts
(2 hunks)packages/api/src/routes/ehr/healthie/appointment-webhook.ts
(1 hunks)packages/api/src/routes/ehr/healthie/auth/middleware.ts
(1 hunks)packages/api/src/routes/ehr/healthie/patient-webhook.ts
(1 hunks)packages/api/src/routes/ehr/healthie/patient.ts
(1 hunks)packages/api/src/routes/ehr/healthie/routes/dash.ts
(1 hunks)packages/api/src/routes/ehr/healthie/routes/webhook.ts
(1 hunks)packages/api/src/routes/ehr/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/athenahealth/patient.ts
(1 hunks)packages/api/src/routes/internal/ehr/elation/patient.ts
(2 hunks)packages/api/src/routes/internal/ehr/healthie/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/healthie/patient.ts
(1 hunks)packages/api/src/routes/internal/ehr/healthie/secret-key.ts
(1 hunks)packages/api/src/routes/internal/ehr/index.ts
(1 hunks)packages/api/src/routes/internal/index.ts
(2 hunks)packages/api/src/routes/internal/jwt-token/healthie.ts
(1 hunks)packages/api/src/routes/internal/jwt-token/index.ts
(1 hunks)packages/api/src/shared/config.ts
(1 hunks)packages/core/src/external/ehr/api/link-patient.ts
(3 hunks)packages/core/src/external/ehr/athenahealth/index.ts
(2 hunks)packages/core/src/external/ehr/elation/index.ts
(4 hunks)packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts
(2 hunks)packages/core/src/external/ehr/healthie/index.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts
(1 hunks)packages/core/src/external/ehr/shared.ts
(2 hunks)packages/core/src/util/config.ts
(1 hunks)packages/core/src/util/webhook.ts
(2 hunks)packages/infra/config/env-config.ts
(1 hunks)packages/infra/lib/api-stack.ts
(3 hunks)packages/infra/lib/api-stack/api-service.ts
(5 hunks)packages/infra/lib/ehr-nested-stack.ts
(6 hunks)packages/infra/lib/secrets-stack.ts
(1 hunks)packages/shared/src/domain/secrets.ts
(1 hunks)packages/shared/src/interface/external/ehr/elation/index.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/appointment.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/event.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/index.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/jwt-token.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/patient.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/subscription.ts
(1 hunks)packages/shared/src/interface/external/ehr/source.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
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 toundefined
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 (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- 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)
notif ('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
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/api/src/routes/ehr/elation/auth/middleware.ts
packages/shared/src/interface/external/ehr/source.ts
packages/core/src/util/config.ts
packages/api/src/routes/internal/jwt-token/index.ts
packages/shared/src/domain/secrets.ts
packages/shared/src/interface/external/ehr/elation/index.ts
packages/api/src/routes/internal/ehr/healthie/index.ts
packages/api/src/routes/ehr/elation/patient.ts
packages/api/src/external/ehr/elation/command/sync-patient.ts
packages/infra/lib/secrets-stack.ts
packages/api/src/routes/internal/ehr/index.ts
packages/api/src/shared/config.ts
packages/api/src/routes/ehr/healthie/routes/webhook.ts
packages/api/src/routes/internal/ehr/elation/patient.ts
packages/infra/config/env-config.ts
packages/api/src/routes/ehr/athenahealth/patient.ts
packages/core/src/external/ehr/athenahealth/index.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts
packages/core/src/external/ehr/shared.ts
packages/core/src/external/ehr/elation/index.ts
packages/api/src/routes/internal/ehr/healthie/secret-key.ts
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts
packages/api/src/routes/ehr/healthie/routes/dash.ts
packages/api/src/routes/internal/index.ts
packages/api/src/routes/ehr/healthie/appointment-webhook.ts
packages/api/src/routes/ehr/healthie/patient-webhook.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
packages/api/src/routes/internal/jwt-token/healthie.ts
packages/api/src/routes/internal/ehr/healthie/patient.ts
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts
packages/api/src/routes/internal/ehr/athenahealth/patient.ts
packages/shared/src/interface/external/ehr/healthie/index.ts
packages/api/src/routes/ehr/healthie/patient.ts
packages/api/src/external/ehr/shared.ts
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
packages/infra/lib/api-stack/api-service.ts
packages/shared/src/interface/external/ehr/healthie/event.ts
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts
packages/core/src/external/ehr/api/link-patient.ts
packages/core/src/util/webhook.ts
packages/api/src/routes/ehr/index.ts
packages/infra/lib/ehr-nested-stack.ts
packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts
packages/api/src/routes/ehr/healthie/auth/middleware.ts
packages/core/src/external/ehr/healthie/index.ts
packages/api/src/external/ehr/healthie/shared.ts
packages/shared/src/interface/external/ehr/healthie/appointment.ts
packages/shared/src/interface/external/ehr/healthie/patient.ts
packages/infra/lib/api-stack.ts
packages/shared/src/interface/external/ehr/healthie/subscription.ts
packages/api/src/external/ehr/healthie/command/sync-patient.ts
🧠 Learnings (1)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/core/src/external/ehr/api/sync-patient.ts:16-55
Timestamp: 2025-03-11T20:42:46.516Z
Learning: In the patient synchronization architecture, the flow follows this pattern: (1) `ehr-sync-patient-cloud.ts` sends messages to an SQS queue, (2) the `ehr-sync-patient` Lambda consumes these messages, and (3) the Lambda uses the `syncPatient` function to make the API calls to process the patient data.
🧬 Code Graph Analysis (19)
packages/core/src/util/config.ts (1)
packages/api/src/shared/config.ts (1)
getEnvVarOrFail
(14-14)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (4)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
HealthieLinkPatientHandler
(7-9)packages/core/src/util/config.ts (1)
Config
(9-198)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
HealthieLinkPatientLocal
(6-22)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
HealthieLinkPatientCloud
(6-26)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
subscriptionResources
(3-3)
packages/api/src/routes/ehr/healthie/appointment-webhook.ts (8)
packages/api/src/routes/helpers/request-logger.ts (1)
requestLogger
(10-63)packages/api/src/routes/util.ts (3)
asyncHandler
(10-28)getCxIdOrFail
(140-144)getFromQueryOrFail
(61-65)packages/shared/src/interface/external/ehr/healthie/event.ts (1)
healthieAppointmentCreatedEventSchema
(3-7)packages/api/src/command/mapping/cx.ts (1)
getCxMappingOrFail
(54-63)packages/shared/src/index.ts (1)
MetriportError
(40-40)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
healthieSecondaryMappingsSchema
(9-19)packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
getHealthiePatientFromAppointment
(11-24)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
updateHealthiePatientQuickNotes
(187-200)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (3)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (2)
HealthieLinkPatientHandler
(7-9)ProcessLinkPatientRequest
(1-5)packages/core/src/external/ehr/api/link-patient.ts (1)
linkPatient
(17-47)packages/shared/src/index.ts (1)
sleep
(13-13)
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
packages/api/src/external/ehr/healthie/shared.ts (1)
createHealthieClient
(101-110)
packages/api/src/routes/internal/jwt-token/healthie.ts (3)
packages/api/src/routes/helpers/request-logger.ts (1)
requestLogger
(10-63)packages/api/src/routes/util.ts (2)
asyncHandler
(10-28)getAuthorizationToken
(181-187)packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (2)
healthieDashSource
(4-4)healthieDashJwtTokenDataSchema
(5-9)
packages/api/src/routes/internal/ehr/healthie/patient.ts (5)
packages/api/src/routes/helpers/request-logger.ts (1)
requestLogger
(10-63)packages/api/src/routes/util.ts (3)
asyncHandler
(10-28)getFromQueryOrFail
(61-65)getFromQueryAsBoolean
(84-86)packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)
processPatientsFromAppointments
(45-169)packages/core/src/util/error/shared.ts (1)
processAsyncError
(56-64)packages/api/src/external/ehr/healthie/command/sync-patient.ts (2)
syncHealthiePatientIntoMetriport
(47-104)updateHealthiePatientQuickNotes
(187-200)
packages/api/src/routes/ehr/healthie/patient.ts (3)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)
tokenEhrPatientIdQueryParam
(19-19)packages/api/src/routes/util.ts (4)
asyncHandler
(10-28)getCxIdOrFail
(140-144)getFrom
(131-133)getFromQueryOrFail
(61-65)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
syncHealthiePatientIntoMetriport
(47-104)
packages/api/src/external/ehr/shared.ts (7)
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (2)
healthieDashSource
(4-4)HealthieDashJwtTokenData
(10-10)packages/shared/src/interface/external/ehr/athenahealth/cx-mapping.ts (2)
AthenaSecondaryMappings
(9-9)athenaSecondaryMappingsSchema
(3-8)packages/shared/src/interface/external/ehr/elation/cx-mapping.ts (2)
ElationSecondaryMappings
(18-18)elationSecondaryMappingsSchema
(9-17)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)
HealthieSecondaryMappings
(20-20)healthieSecondaryMappingsSchema
(9-19)packages/shared/src/interface/external/ehr/source.ts (1)
EhrSource
(8-8)packages/shared/src/common/date.ts (1)
buildDayjs
(70-72)packages/shared/src/index.ts (1)
BadRequestError
(39-39)
packages/infra/lib/api-stack/api-service.ts (1)
packages/infra/lib/shared/sqs.ts (1)
provideAccessToQueue
(219-233)
packages/core/src/external/ehr/api/link-patient.ts (1)
packages/core/src/external/ehr/api/api-shared.ts (1)
ApiBaseParams
(3-9)
packages/core/src/util/webhook.ts (1)
packages/shared/src/index.ts (1)
MetriportError
(40-40)
packages/api/src/routes/ehr/index.ts (1)
packages/api/src/routes/middlewares/auth.ts (1)
checkMAPIAccess
(46-65)
packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts (5)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (2)
SubscriptionResource
(4-4)SubscriptionWithSignatureSecret
(28-28)packages/api/src/command/mapping/cx.ts (2)
getCxMappingOrFail
(54-63)setSecondaryMappingsOnCxMappingById
(147-164)packages/shared/src/index.ts (1)
MetriportError
(40-40)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
healthieSecondaryMappingsSchema
(9-19)packages/api/src/external/ehr/healthie/shared.ts (1)
createHealthieClient
(101-110)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (7)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
isSubscriptionResource
(5-7)packages/api/src/external/ehr/healthie/shared.ts (1)
getHealthieSecretKeyInfo
(112-139)packages/core/src/util/webhook.ts (1)
verifySignature
(76-95)packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)
healthieDashSource
(4-4)packages/api/src/routes/util.ts (1)
getAuthorizationToken
(181-187)packages/shared/src/common/date.ts (1)
buildDayjs
(70-72)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
shortDurationTokenDuration
(37-37)
packages/core/src/external/ehr/healthie/index.ts (6)
packages/core/src/util/config.ts (1)
Config
(9-198)packages/core/src/external/ehr/shared.ts (3)
MakeRequestParamsInEhr
(79-82)makeRequest
(84-202)formatDate
(55-61)packages/shared/src/index.ts (2)
MetriportError
(40-40)NotFoundError
(41-41)packages/shared/src/interface/external/ehr/healthie/patient.ts (7)
Patient
(23-23)PatientGraphql
(29-29)patientGraphqlSchema
(24-28)PatientQuickNotesGraphql
(41-41)patientQuickNotesGraphqlSchema
(36-40)PatientUpdateQuickNotesGraphql
(50-50)patientUpdateQuickNotesGraphqlSchema
(43-49)packages/shared/src/interface/external/ehr/healthie/appointment.ts (5)
AppointmentWithAttendee
(10-12)AppointmentListResponseGraphql
(30-30)appointmentListResponseGraphqlSchema
(27-29)AppointmentGetResponseGraphql
(21-21)appointmentGetResponseGraphqlSchema
(18-20)packages/shared/src/interface/external/ehr/healthie/subscription.ts (5)
Subscription
(24-24)SubscriptionResource
(4-4)SubscriptionWithSignatureSecret
(28-28)SubscriptionWithSignatureSecretGraphql
(36-38)subscriptionWithSignatureSecretGraphqlSchema
(29-35)
packages/api/src/external/ehr/healthie/shared.ts (9)
packages/shared/src/index.ts (4)
BadRequestError
(39-39)toTitleCase
(15-15)MetriportError
(40-40)NotFoundError
(41-41)packages/api/src/shared/config.ts (2)
getHealthieEnv
(356-358)Config
(16-366)packages/api/src/external/ehr/shared.ts (2)
EhrPerPracticeParams
(179-179)EhrEnvAndApiKey
(63-66)packages/core/src/external/ehr/healthie/index.ts (2)
HealthieEnv
(40-40)isHealthieEnv
(41-43)packages/core/src/util/config.ts (1)
Config
(9-198)packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
SubscriptionResource
(4-4)packages/api/src/command/mapping/cx.ts (1)
getCxMappingOrFail
(54-63)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
healthieSecondaryMappingsSchema
(9-19)packages/shared/src/domain/secrets.ts (1)
cxApiKeyMapSecretSchema
(4-4)
packages/shared/src/interface/external/ehr/healthie/appointment.ts (1)
packages/api/src/external/ehr/shared.ts (1)
Appointment
(127-131)
🪛 Biome (1.9.4)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
[error] 60-60: Avoid the use of spread (...
) syntax on accumulators.
Spread syntax should be avoided on accumulators (like those in .reduce
) because it causes a time complexity of O(n^2)
.
Consider methods such as .splice or .push instead.
(lint/performance/noAccumulatingSpread)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
🔇 Additional comments (74)
packages/api/src/routes/ehr/elation/auth/middleware.ts (1)
1-1
: Import ordering improvement.Moving the
verifyWebhookSignatureEd25519
import to the top maintains consistency with other EHR middleware modules, including the new Healthie implementation. This change doesn't affect functionality but improves code organization.packages/shared/src/domain/secrets.ts (1)
4-4
: Good addition of Healthie API key schema.The new
cxApiKeyMapSecretSchema
provides type validation for Healthie API keys, supporting the integration's API client creation and webhook subscription functionality. This follows the same pattern as the existing schema while representing a distinct semantic entity.packages/shared/src/interface/external/ehr/source.ts (1)
5-5
: Good implementation of Healthie EHR source.Adding Healthie to the
EhrSources
enum properly integrates it into the type system. The implementation is clean as theehrSources
array andEhrSource
type automatically incorporate the new value without requiring additional changes.packages/api/src/routes/internal/jwt-token/index.ts (1)
5-5
: Clean integration of Healthie JWT token routes.The addition of Healthie JWT token routes follows the established pattern for other EHR integrations, maintaining consistency in the codebase. This change properly integrates Healthie token management into the API's routing structure without modifying existing functionality.
Also applies to: 13-13
packages/core/src/util/config.ts (1)
180-182
: Implementation follows established patterns.The method implementation correctly follows the project's pattern for environment variable retrieval and is appropriately positioned near similar EHR-related methods.
packages/api/src/external/ehr/elation/command/sync-patient.ts (2)
12-12
: LGTM: Necessary dayjs plugin import.Adding the duration plugin import is necessary for the token duration functionality in this file.
28-28
: Correctly extends dayjs with the duration plugin.The extension is properly placed before using duration-related functionality in the constants below.
packages/infra/lib/secrets-stack.ts (1)
65-65
: Correctly adds Healthie secrets following the established pattern.The implementation properly integrates Healthie secrets into the AWS Secrets Manager provisioning flow, consistent with how other EHR integrations are handled.
packages/api/src/routes/internal/ehr/healthie/index.ts (1)
1-10
: Well-structured router with clean separation of concerns.The implementation follows Express best practices with a modular approach, separating patient and secret-key routes into their own sub-routers. The code is concise and follows the project's functional programming style.
packages/core/src/external/ehr/shared.ts (2)
102-104
: Improved header case-insensitivity handlingThis change properly handles case-insensitivity for Content-Type headers, which is good since HTTP headers are case-insensitive according to HTTP specifications, but implementations can vary.
118-118
: Good defensive URL handlingThe defensive check to only include the URL if it's not an empty string prevents potential issues with empty URLs in Axios requests.
packages/api/src/routes/internal/ehr/index.ts (1)
5-5
: LGTM - Consistent integration of Healthie routesThe additions follow the established pa 6D47 ttern for integrating EHR providers, maintaining consistency with the existing implementation for other providers (athena, elation, canvas).
Also applies to: 12-12
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts (1)
2-3
: LGTM - Good abstraction for multi-EHR supportThe changes effectively refactor the
linkPatient
function to support multiple EHR sources dynamically, improving code reusability and maintainability. Explicitly specifyingEhrSources.elation
makes the code more self-documenting.Also applies to: 15-15
packages/api/src/routes/ehr/healthie/routes/webhook.ts (1)
1-10
: Clean webhook route implementationThe implementation follows good patterns with:
- Using express-promise-router for automatic promise handling
- Clear organization with dedicated sub-routers for different webhook types
- Thoughtful route structure using path parameters
packages/core/src/external/ehr/athenahealth/index.ts (2)
178-178
: Improved HTTP header case consistency.Updated the "Content-Type" header to use proper capitalization, which follows standard HTTP header conventions.
211-211
: Standardized HTTP header capitalization.Consistent with the earlier change, updated the "Content-Type" header to use proper capitalization in the axios instance configuration.
packages/shared/src/interface/external/ehr/elation/index.ts (1)
6-7
: Expanded Elation exports to include cx-mapping and event modules.Added exports for cx-mapping and event modules to enable centralized imports from the Elation interface. This aligns with the pattern used by other EHR integrations including the new Healthie integration.
packages/core/src/external/ehr/elation/index.ts (4)
100-100
: Standardized HTTP header capitalization.Updated the "Content-Type" header to use proper capitalization in the token request, which follows standard HTTP header conventions.
131-131
: Improved HTTP header case consistency.Standardized the "Content-Type" header capitalization in the axios instance initialization.
176-176
: Added explicit Content-Type header for PATCH request.Explicitly specified the "Content-Type" header for the PATCH request to update patient metadata, ensuring the server correctly interprets the request body as JSON.
229-229
: Added explicit Content-Type header for POST request.Added the "Content-Type" header for the problem creation POST request, ensuring proper content negotiation with the Elation API.
packages/infra/config/env-config.ts (1)
260-265
: Added Healthie configuration to support new EHR integration.Added the configuration block for Healthie in the ehrIntegration section with environment and API key map settings. This supports the Healthie integration being introduced in this PR.
The structure follows the pattern established for other EHR integrations (athenaHealth, elation, canvas) with appropriate environment and secrets configuration.
packages/api/src/shared/config.ts (1)
356-361
: Implementation correctly follows the pattern of other EHR configurations.The addition of
getHealthieEnv
andgetHealthieApiKeyMap
methods is consistent with the existing pattern for other EHR providers (like Athena, Elation, Canvas) and follows the codebase's convention for environment variable access.packages/api/src/routes/ehr/elation/patient.ts (2)
18-18
: Documentation improvement is accurate and helpful.The added documentation for the
practiceId
query parameter clearly specifies its purpose as "The ID of Elation Practice", which matches the implementation that usesgetFromQueryOrFail("practiceId", req)
in line 29.
44-44
: Documentation improvement is accurate and consistent.The added documentation for the
practiceId
query parameter in the POST route matches what was added for the GET route, maintaining consistency between the two endpoint definitions.packages/api/src/routes/ehr/index.ts (3)
17-22
: Clear and consistent implementation of middleware imports.The import statements for Healthie middleware and routes follow the same pattern used for other EHR providers, using appropriate naming conventions to ensure clarity of purpose for each component.
29-29
: Route registration follows established pattern.The Healthie dashboard route is correctly registered with the same middleware chain as other EHR providers: first the customer ID processing middleware, then the MAPI access check, and finally the route handler.
33-33
: Webhook route registration is properly implemented.The Healthie webhook route is correctly registered with the appropriate middleware chain, consistent with the pattern used for other EHR providers' webhook routes.
packages/shared/src/interface/external/ehr/healthie/index.ts (1)
1-6
: Well-structured exports follow the barrel pattern.The module correctly uses the barrel pattern to re-export all entities from related Healthie interface modules, making it easy to import all Healthie-related types with a single import statement. This approach is clean and maintainable.
packages/api/src/routes/ehr/athenahealth/patient.ts (2)
16-17
: JSDoc improvement - query parameter documentation added.The documentation now properly includes these athenahealth-specific query parameters, which improves code clarity and developer experience.
44-45
: JSDoc improvement - query parameter documentation added.The documentation for the POST endpoint now correctly specifies the required query parameters, matching the implementation.
packages/api/src/routes/internal/ehr/athenahealth/patient.ts (1)
71-75
: Documentation accurately reflects the implementation parameters.The JSDoc comments now properly document all query parameters used in the route handler:
cxId
,patientId
,practiceId
,departmentId
, andtriggerDq
. This matches the parameters extracted in the implementation (lines 82-86).packages/api/src/routes/internal/index.ts (2)
35-36
: Good renaming for clearer function imports.Renaming the imports with more specific names (subscribeToElationWebhooks and subscribeToHealthieWebhooks) makes the code more readable and maintainable by clearly indicating which EHR system each function applies to.
331-334
: Feature addition: Healthie webhook subscription support.The code now handles webhook subscriptions for both Elation and Healthie sources, properly extending the system to support the new Healthie integration. The conditional logic is clear and follows the same pattern as the existing Elation implementation.
packages/api/src/routes/internal/ehr/elation/patient.ts (2)
37-41
: Documentation accurately reflects implementation parameters.The JSDoc comments now correctly document all query parameters used in the route handler:
cxId
,patientId
,practiceId
, and the optionaltriggerDq
flag. This matches how the parameters are extracted and used in the implementation (lines 47-56).
65-68
: Documentation accurately reflects implementation parameters.The JSDoc comments now properly document all query parameters used in the patient linking route handler. This improves code clarity and consistency across similar endpoints.
packages/infra/lib/api-stack.ts (3)
416-418
: Correct integration of Healthie resources.The Healthie link patient queue and lambda are properly destructured from the EHR nested stack, following the same pattern as other EHR integrations.
545-545
: Queue properly included in API service.The Healthie link patient queue is correctly added to the API service configuration.
640-640
: Lambda correctly added to API URL recipients.The Healthie link patient lambda is properly added to the list of lambdas that receive the API_URL environment variable.
packages/api/src/routes/internal/ehr/healthie/secret-key.ts (1)
20-32
: Well-structured route implementation with proper validation.The endpoint correctly validates inputs and handles the secret key retrieval with appropriate error handling. The use of asyncHandler ensures that async errors are properly captured and propagated.
packages/api/src/routes/ehr/healthie/routes/dash.ts (1)
1-37
: Well-structured router with proper middleware organization.The implementation follows the established patterns used in other EHR integrations. The router effectively organizes routes for patient data, medical documents, and settings with appropriate middleware chains for each endpoint.
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
1-9
: Clean type and interface definition for patient linking.The exported type
ProcessLinkPatientRequest
and interfaceHealthieLinkPatientHandler
provide a clear contract for implementing patient linking functionality. This follows TypeScript best practices with proper naming conventions and clear type definitions.packages/infra/lib/api-stack/api-service.ts (1)
112-112
: Properly integrates Healthie queue resources into the API service.The changes correctly add the Healthie link patient queue to the API service by:
- Adding the queue parameter to the function signature
- Including the queue URL in environment variables
- Setting up the Healthie environment configuration
- Granting the necessary SQS send permissions to the task role
These changes maintain consistency with the existing patterns used for other EHR integrations.
Also applies to: 154-154, 289-289, 339-339, 452-456
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (2)
4-9
: Good type definition with clear parametersThe type definition is properly structured with clear parameter names and types, following the TypeScript guidelines. The optional API parameter allows for dependency injection, which is good for testability.
11-16
: Function signature follows best practicesThe function signature properly destructures the parameters object, which aligns with the coding guidelines of preferring deconstructing parameters for functions instead of multiple parameters.
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (2)
6-8
: Class implementation follows interface contractThe class correctly implements the
HealthieLinkPatientHandler
interface and declares a private SQS client property.
9-15
: Good constructor with fallback initializationThe constructor properly initializes the SQS client with fallback to default AWS region if not provided. The use of the nullish coalescing operator aligns with the coding guidelines.
packages/core/src/external/ehr/api/link-patient.ts (5)
7-7
: Good type definition using OmitThe type definition uses
Omit
to leverage the existingApiBaseParams
type, which follows the DRY principle.
12-12
: Updated JSDoc to reflect the parameter changeThe JSDoc is properly updated to include documentation for the new
ehr
parameter.
18-18
: Function parameter update maintains backward compatibilityThe function now accepts an
ehr
parameter, making it more generic and reusable for different EHR sources.
30-30
: Dynamic URL construction based on EHR sourceThe URL is now dynamically constructed based on the EHR parameter, which is more flexible than the previous hardcoded approach.
39-44
: Enhanced error metadata with EHR sourceError handling now includes the EHR source in the error metadata, which improves debuggability. The context is also more generic, which aligns with the function's broader scope.
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (3)
4-4
: Good use of constants for EHR sourceUsing a constant for the EHR source ensures consistency across the codebase and makes maintenance easier if the source name changes.
5-9
: Well-structured schema with appropriate validationsThe schema is well-defined with appropriate validations for the required fields. The use of
z.literal
for the source field ensures type safety.
10-10
: Type definition inferred from schemaThe type is properly inferred from the Zod schema, which ensures type safety and consistency between runtime validation and compile-time types.
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
4-7
: Webhook schema implementation looks good.The webhook schema defines the required fields for Healthie webhooks with appropriate validation.
packages/core/src/util/webhook.ts (3)
35-45
: HMAC implementation looks good.The implementation of the
getSigningKey
function correctly imports a raw secret key as a CryptoKey for HMAC-SHA256 signing according to the Healthie documentation.
47-66
: Data signing construction follows security best practices.The function correctly extracts the content digest from headers and constructs the data to sign with all required components for proper HMAC verification.
68-74
: Hex encoding implementation is clean and efficient.The signature generation function properly uses the Web Crypto API and converts the result to a hex string using a functional approach.
packages/api/src/routes/ehr/healthie/patient-webhook.ts (4)
1-14
: Import organization looks good.The imports are well-organized, grouped by external dependencies and internal modules, which enhances readability.
17-22
: Clear JSDoc for the endpoint.The documentation clearly describes the endpoint's purpose and expected response.
23-62
: Route handler implementation follows best practices.The route handler correctly:
- Uses middleware for parameter handling, request logging, and error handling
- Validates input parameters and the request body
- Checks configuration flags to determine processing behavior
- Returns appropriate HTTP responses
50-59
: Validation between quick notes update and patient processing.The code allows for updating quick notes even when patient processing is disabled. Confirm this is the intended behavior as it might lead to inconsistent states if the quick notes contain references to processing that didn't occur.
packages/infra/lib/ehr-nested-stack.ts (4)
18-18
: Wait time constant follows existing pattern.The new constant
waitTimeHealthieLinkPatient
aligns with the existing constants used for rate limiting.
26-26
: New Healthie link patient configuration follows existing structure.The Healthie link patient settings follow the same structure as existing configurations, which maintains consistency in the codebase.
Also applies to: 73-93, 166-166
186-187
: Property declarations align with existing patterns.The new properties
healthieLinkPatientLambda
andhealthieLinkPatientQueue
are correctly declared as readonly, following the pattern of existing properties.
235-243
: Constructor setup follows existing pattern.The Healthie link patient setup in the constructor correctly follows the established pattern for other EHR integrations.
packages/api/src/routes/ehr/healthie/appointment-webhook.ts (4)
1-15
: Import organization aligns with the patient webhook.The imports follow the same organization pattern as the patient webhook file, maintaining consistency.
16-23
: Router and documentation follow consistent patterns.The router setup and JSDoc follow the same structure as other webhook endpoints.
24-69
: Route handler structure is consistent with other webhook handlers.The implementation follows the same pattern as the patient webhook handler:
- Uses the same middleware
- Extracts and validates parameters
- Processes the webhook based on configuration flags
- Returns appropriate HTTP responses
56-58
: Code structure allows for early returns.The use of early returns when processing should be skipped makes the code more readable and reduces nesting.
Also applies to: 42-45
packages/api/src/external/ehr/shared.ts (1)
51-53
: Zero-delay between practice batches may overload the Healthie API
delayBetweenPracticeBatches
was reduced to0 seconds
. WithparallelPractices = 10
, every batch against every practice will fire at once.
Healthie’s rate-limits are unknown here, but even Athena/Canvas integrations keep at least a small pause to avoid 429s.Consider keeping a modest delay (e.g. 250-500 ms) or making it configurable per-EHR so we can quickly tune it without a code change.
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)
121-123
: Deduplication key might be too coarse
uniqBy(allAppointments, "patientId")
removes duplicates solely onpatientId
.
If a patient can appear in multiple practices (or multiple cxIds) we will process only the first occurrence, potentially skipping necessary linking/sync for the other practices.Verify whether Healthie patient IDs are globally unique across practices; if not, consider a composite key:
uniqBy(allAppointments, a => `${a.practiceId}-${a.patientId}`)packages/core/src/external/ehr/healthie/index.ts (1)
195-199
: TODO is out-of-sync with implementationThe comment says “Remove existingQuickNotes and link from the mutation” but the variable still concatenates
scrubbedExistingQuickNotes
andlinkElement
.
Either remove the TODO or adjust the mutation variables to reflect the intended behaviour.
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
Show resolved
Hide resolved
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
Show resolved
Hide resolved
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
Show resolved
Hide resolved
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
packages/core/src/external/ehr/healthie/index.ts (2)
274-282
: Preserve all attendees instead of truncating to oneMapping every appointment to only its first attendee discards valid data and silently corrupts downstream processing if multiple attendees exist.
- return appointments.map(appointment => { - const attendee = appointment.attendees[0]; - if (!attendee) { - throw new MetriportError("Appointment has no attendees", undefined, additionalInfo); - } - return { ...appointment, attendees: [attendee] }; - }); + return appointments.map(appointment => { + if (!appointment.attendees?.length) { + throw new MetriportError("Appointment has no attendees", undefined, additionalInfo); + } + // retain the full attendee list + return appointment as AppointmentWithAttendee; + });
336-352
: Fix invalid GraphQL query & variable mismatch
query webhooks()
is syntactically invalid—GraphQL operation names should not have empty parentheses, and the$page_size
variable you pass is never declared/used inside the query. This will 400-fail at runtime.- const query = `query webhooks() { + const query = `query webhooks($page_size: Int) { webhooks( order_by: CREATED_AT_ASC + page_size: $page_size ) {If pagination is not actually required, drop both the variable and the
variables
object instead.
🧹 Nitpick comments (3)
packages/core/src/external/ehr/healthie/index.ts (3)
195-195
: Address the TODO comment before mergingThere's a TODO comment about removing existingQuickNotes and link from the mutation. Consider addressing this before finalizing the implementation.
176-179
: Consider optimizing the regex for scrubbing quick notesThe current regex pattern to remove existing Metriport Integration links is quite general and might match more than intended. Consider making it more specific to your link format.
- const scrubbedExistingQuickNotes = existingQuickNotes?.replace( - /<p><a .*>Metriport Integration<\/a><\/p>/g, - "" - ); + const scrubbedExistingQuickNotes = existingQuickNotes?.replace( + /<p><a href="[^"]*" target="_blank">Metriport Integration<\/a><\/p>/g, + "" + );
383-397
: Simplify subscription existence checkThe current implementation for checking if a subscription exists is quite verbose. Consider simplifying it using Array.some() with a destructured parameter for better readability.
- if ( - existingSubscriptions.some(subscription => { - const isUrlMatch = subscription.url === url; - const isResourceMatch = - subscription.event_type === resource || - subscription.webhook_events?.some(event => event.event_type === resource); - return isUrlMatch && isResourceMatch; - }) - ) { + if (existingSubscriptions.some(({ url: subUrl, event_type, webhook_events }) => + subUrl === url && ( + event_type === resource || + webhook_events?.some(event => event.event_type === resource) + ) + )) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (3)
packages/api/src/routes/ehr/healthie/auth/middleware.ts
(1 hunks)packages/api/src/routes/ehr/healthie/routes/webhook.ts
(1 hunks)packages/core/src/external/ehr/healthie/index.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/api/src/routes/ehr/healthie/routes/webhook.ts
- packages/api/src/routes/ehr/healthie/auth/middleware.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
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 toundefined
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 (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- 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)
notif ('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
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/core/src/external/ehr/healthie/index.ts
🔇 Additional comments (4)
packages/core/src/external/ehr/healthie/index.ts (4)
80-123
: LGTM: Well-structured patient retrieval implementationThe getPatient method is well-structured with appropriate error handling, schema validation, and use of the GraphQL API. The approach to patient data retrieval is done correctly.
125-159
: LGTM: Clean implementation of getPatientQuickNotes methodThis method follows the established pattern for API interaction with proper error handling, validation, and use of null coalescing for the return value.
161-214
: LGTM: Effective link management in updatePatientQuickNotesWithLinkDespite the TODO comment, the implementation correctly handles removing existing Metriport links before adding the new one, preventing duplication.
446-480
: LGTM: Well-abstracted utility methodsThe makeRequest and formatDate utility methods are well abstracted and follow the coding guidelines by being small, deterministic functions with clear inputs and outputs.
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/core/src/external/ehr/healthie/index.ts (1)
336-351
:⚠️ Potential issueFix invalid GraphQL query syntax
The GraphQL query has empty parentheses after the operation name, which is syntactically invalid and will cause runtime errors.
- const query = `query webhooks() { + const query = `query webhooks { webhooks( order_by: CREATED_AT_ASC ) {
🧹 Nitpick comments (5)
packages/core/src/external/ehr/healthie/index.ts (5)
54-56
: Optimize constructor and initialize methodThe constructor creates an axios instance that's immediately replaced in the initialize method. This creates unnecessary object instantiation.
private constructor(config: HealthieApiConfig) { this.apiKey = config.apiKey; this.practiceId = config.practiceId; - this.axiosInstance = axios.create({}); this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`; }
257-263
: Consider implementing pagination handlingThe current implementation has a hard limit of 1000 appointments and doesn't handle scenarios where more appointments exist than the page size allows.
Consider implementing a pagination mechanism to fetch all appointments when there are more than the page size limit. This would make the API more robust for practices with high appointment volumes.
const variables = { startDate: this.formatDate(startAppointmentDate.toISOString()) ?? "", endDate: this.formatDate(endAppointmentDate.toISOString()) ?? "", offset: 0, page_size: 1000, }; + // TODO: Implement pagination to handle more than 1000 appointments
382-397
: Improve subscription existence checkThe current check for existing subscriptions is complex and could be simplified. Additionally, a warning or status update would be more user-friendly than throwing an error when a subscription already exists.
const existingSubscriptions = await this.getSubscriptions({ cxId }); - if ( - existingSubscriptions.some(subscription => { - const isUrlMatch = subscription.url === url; - const isResourceMatch = - subscription.event_type === resource || - subscription.webhook_events?.some(event => event.event_type === resource); - return isUrlMatch && isResourceMatch; - }) - ) { - throw new MetriportError( - "Subscription already exists for resource", - undefined, - additionalInfo - ); + const existingSubscription = existingSubscriptions.find(subscription => { + const isUrlMatch = subscription.url === url; + const isResourceMatch = + subscription.event_type === resource || + subscription.webhook_events?.some(event => event.event_type === resource); + return isUrlMatch && isResourceMatch; + }); + + if (existingSubscription) { + debug("Subscription already exists for resource", { subscriptionId: existingSubscription.id }); + return existingSubscription as SubscriptionWithSignatureSecret; }
446-475
: Consider adding retry logic for API requestsThe makeRequest method doesn't include retry logic for transient failures, which can be common in network requests to external APIs.
Consider adding retry logic with exponential backoff for API requests to handle transient failures more gracefully. This is especially important for webhook operations that are critical for maintaining data synchronization.
29-29
: Avoid using Config class directlyDirectly using the Config class creates tight coupling and makes testing more difficult.
Consider injecting the API URL through the constructor or create method parameters instead of accessing it directly from the Config class. This would improve testability and flexibility.
-const apiUrl = Config.getApiUrl(); interface HealthieApiConfig extends Omit<ApiConfig, "twoLeggedAuthTokenInfo" | "clientKey" | "clientSecret"> { apiKey: string; environment: HealthieEnv; + apiUrl?: string; } class HealthieApi { private axiosInstance: AxiosInstance; private baseUrl: string; private apiKey: string; private practiceId: string; + private apiUrl: string; private constructor(config: HealthieApiConfig) { this.apiKey = config.apiKey; this.practiceId = config.practiceId; this.baseUrl = `https://${config.environment}.gethealthie.com/graphql`; + this.apiUrl = config.apiUrl ?? Config.getApiUrl(); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (1)
packages/core/src/external/ehr/healthie/index.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
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 toundefined
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 (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- 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)
notif ('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
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/core/src/external/ehr/healthie/index.ts
🧠 Learnings (1)
packages/core/src/external/ehr/healthie/index.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3754
File: packages/core/src/external/ehr/healthie/index.ts:274-282
Timestamp: 2025-04-30T21:07:37.904Z
Learning: In the Healthie integration, appointments are assumed to have one attendee of interest, and the code is designed to extract and work with only the first attendee from each appointment.
🔇 Additional comments (2)
packages/core/src/external/ehr/healthie/index.ts (2)
274-282
: Appointment attendee handling implementationThe current implementation assumes appointments have one attendee of interest and extracts only the first attendee from each appointment.
Based on the retrieved learnings, this is an intentional design choice. The code correctly extracts the first attendee and throws an appropriate error if no attendees exist.
324-329
: Consistent attendee handling in getAppointment methodSimilar to getAppointments, this method extracts only the first attendee from an appointment.
This is consistent with the design choice noted in the retrieved learnings, where appointments are assumed to have one attendee of interest.
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
packages/lambdas/src/healthie-link-patient.ts (3)
28-28
: Replace direct console.log with proper logging utilityAccording to your coding guidelines, you should "avoid using
console.log
andconsole.error
in packages other than utils, infra and shared, and try to useout().log
instead".- console.log(`Running with unparsed body: ${message.body}`); + log(`Running with unparsed body: ${message.body}`);Note: This change requires moving the prefixed log declaration above this line.
33-33
: Avoid multi-line logs with JSON.stringifyThe coding guidelines specify: "Avoid multi-line logs - don't create multi-line strings when using
JSON.stringify()
".- log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`); + log(`Parsed: cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}, waitTimeInMillis ${waitTimeInMillis}`);
32-33
: Move log declaration to optimize code structureThe prefixed log declaration should be moved above line 28 to allow its use for all logging operations in this Lambda.
const parsedBody = parseBody(message.body); const { cxId, practiceId, patientId } = parsedBody; const log = prefixedLog(`cxId ${cxId}, practiceId ${practiceId}, patientId ${patientId}`); - log(`Parsed: ${JSON.stringify(parsedBody)}, waitTimeInMillis ${waitTimeInMillis}`); + log(`Parsed successfully, waitTimeInMillis ${waitTimeInMillis}`);packages/lambdas/src/shared/ehr.ts (1)
58-60
: Consider adding a discriminator property to differentiate EHR sourcesThe function returns a union type without a clear way to distinguish which EHR system the data belongs to. Consider adding a discriminator property or source indicator to help downstream code safely handle the different types.
export function parseLinkPatient( bodyAsJson: LinkPatientPayload ): ElationProcessLinkPatientRequest | HealthieProcessLinkPatientRequest {You could add a source parameter to the function or consider extending the interface to include a source field:
interface LinkPatientPayload { cxId: unknown; practiceId: unknown; patientId: unknown; source?: unknown; // Optional source identifier }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (3)
packages/lambdas/src/elation-link-patient.ts
(2 hunks)packages/lambdas/src/healthie-link-patient.ts
(1 hunks)packages/lambdas/src/shared/ehr.ts
(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- packages/lambdas/src/elation-link-patient.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
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 toundefined
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 (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- 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)
notif ('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
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/lambdas/src/healthie-link-patient.ts
packages/lambdas/src/shared/ehr.ts
🧬 Code Graph Analysis (1)
packages/lambdas/src/healthie-link-patient.ts (5)
packages/lambdas/src/shared/sqs.ts (1)
getSingleMessageOrFail
(62-83)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
HealthieLinkPatientLocal
(6-22)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
ProcessLinkPatientRequest
(1-5)packages/shared/src/index.ts (1)
MetriportError
(40-40)packages/lambdas/src/shared/ehr.ts (1)
parseLinkPatient
(58-78)
🔇 Additional comments (3)
packages/lambdas/src/healthie-link-patient.ts (1)
46-49
: 🛠️ Refactor suggestionAdd error handling for JSON parse operation
The
JSON.parse
call could throw an exception if the body isn't valid JSON. Add try/catch to handle this gracefully and provide context in the error message.- const bodyAsJson = JSON.parse(bodyString); + let bodyAsJson; + try { + bodyAsJson = JSON.parse(bodyString); + } catch (error) { + throw new MetriportError("Invalid JSON in message body", error); + }⛔ Skipped due to learnings
Learnt from: thomasyopes PR: metriport/metriport#3608 File: packages/lambdas/src/ehr-compute-resource-diff-bundles.ts:58-62 Timestamp: 2025-04-23T19:00:49.707Z Learning: For Lambda functions in the metriport codebase, the team prefers to let the default Lambda error handling catch JSON parsing errors rather than adding explicit try/catch blocks.
packages/lambdas/src/shared/ehr.ts (2)
3-3
: LGTM - Good job adding Healthie supportThe import for Healthie's
ProcessLinkPatientRequest
and the type renaming approach keeps the code clean and maintainable while supporting both EHR sources.
52-56
: LGTM - Good job generalizing the interfaceRenaming from the specific
ElationLinkPatientPayload
to the genericLinkPatientPayload
aligns well with supporting multiple EHR systems.
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82
Ref: #1040
Issues:
Dependencies
Description
Testing
Release Plan
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Documentation
Chores
Bug Fixes