From 6e0135b2c069222164aa4ec969f8db6f6fac1bdb Mon Sep 17 00:00:00 2001 From: Everest - Osa Omigie Date: Mon, 28 Sep 2026 15:31:00 -0400 Subject: [PATCH] feat(event): Add order.checkoutStarted event Adds the `order.checkoutStarted` event type so partners can listen for the hand-off to the prac-pay lab checkout from the embed. - `EVENT_TYPES` gains `order.checkoutStarted`; `OrderPayload` matches the payload emitted by hw-admin (EVE-1825). - `EventListenerPayload` now resolves its `data` through an `EventPayloads` lookup instead of a nested conditional, so a new entry in `EVENT_TYPES` without a matching payload is a compile error rather than silently falling back to `TreatmentPlanPayload`. - `errorMessage.invalidEventType` picks the new type up automatically via `common.availableEventTypes`. - Both demos register a listener for the new event. EVE-1834 Co-Authored-By: Claude Opus 5 --- demo/javascript-demo/client/index.html | 1 + demo/react-demo/src/App.tsx | 5 ++ src/feature/eventType.ts | 19 +++- src/feature/feature.spec.ts | 89 +++++++++++-------- src/feature/index.ts | 1 + .../fullscriptJsValidator.spec.ts | 13 +-- src/index.ts | 1 + 7 files changed, 84 insertions(+), 45 deletions(-) diff --git a/demo/javascript-demo/client/index.html b/demo/javascript-demo/client/index.html index 12a5c05..5bacdd3 100644 --- a/demo/javascript-demo/client/index.html +++ b/demo/javascript-demo/client/index.html @@ -33,6 +33,7 @@

This is a demo app using Fullscript.js with a script tag

feature.mount("treatment-plan-iframe"); feature.on("treatmentPlan.activated", mockCallback); feature.on("patient.selected", mockCallback); + feature.on("order.checkoutStarted", mockCallback); } function mockCallback(data) { diff --git a/demo/react-demo/src/App.tsx b/demo/react-demo/src/App.tsx index 8891e84..86c14fc 100644 --- a/demo/react-demo/src/App.tsx +++ b/demo/react-demo/src/App.tsx @@ -18,6 +18,10 @@ const App = () => { console.log(payload); }; + const handleOrderCheckoutStarted = (payload: EventListenerPayload<"order.checkoutStarted">) => { + console.log(payload); + }; + const mountTreatmentPlan = () => { // ################ // publicKey: fullscript-js-test-public-key-prac @@ -40,6 +44,7 @@ const App = () => { feature.mount("treatment-plan-iframe"); feature.on("treatmentPlan.activated", handleTreatmentPlanActivated); feature.on("patient.selected", handlePatientSelected); + feature.on("order.checkoutStarted", handleOrderCheckoutStarted); }; return ( diff --git a/src/feature/eventType.ts b/src/feature/eventType.ts index 01d7b5e..52a7599 100644 --- a/src/feature/eventType.ts +++ b/src/feature/eventType.ts @@ -2,6 +2,7 @@ const EVENT_TYPES = { "treatmentPlan.activated": "treatmentPlan.activated", "patient.selected": "patient.selected", + "order.checkoutStarted": "order.checkoutStarted", }; type EventType = keyof typeof EVENT_TYPES; @@ -60,10 +61,25 @@ type TreatmentPlanPayload = { }; }; +type OrderPayload = { + order: { + id: string; + practitionerPay: boolean; + treatmentPlan: TreatmentPlanPayload["treatmentPlan"]; + }; +}; + +// Indexing with `E` means adding an entry to EVENT_TYPES without one here is a compile error +type EventPayloads = { + "treatmentPlan.activated": TreatmentPlanPayload; + "patient.selected": PatientPayload; + "order.checkoutStarted": OrderPayload; +}; + type EventListenerPayload = { id: string; type: E; - data: E extends "patient.selected" ? PatientPayload : TreatmentPlanPayload; + data: EventPayloads[E]; createdAt: string; clinicId: string; oauth: { @@ -84,6 +100,7 @@ export { EventListenerPayload, TreatmentPlanPayload, PatientPayload, + OrderPayload, EventType, EVENT_TYPES, }; diff --git a/src/feature/feature.spec.ts b/src/feature/feature.spec.ts index 4c27dca..3916880 100644 --- a/src/feature/feature.spec.ts +++ b/src/feature/feature.spec.ts @@ -2,6 +2,7 @@ import { Dispatcher, createDispatcher } from "../eventSystem"; import { FeatureType } from "../feature"; import { FullscriptOptions } from "../fullscript"; +import { EventType } from "./eventType"; import { FeatureOptions } from "./featureType"; let mockCreateIframe; @@ -153,44 +154,54 @@ describe("feature", () => { }); describe("dispatcher calls", () => { - it("calls registerEventListener on the dispatcher when on is called", () => { - return import("./feature").then(({ getFeature }) => { - const callback = jest.fn(); - const eventType = "treatmentPlan.activated"; - dispatcher.registerEventListener = jest.fn(); - const feature = getFeature( - mockFeatureType, - mockFeatureOptions, - mockFullscriptOptions, - dispatcher - ); - - feature.on(eventType, callback); - expect(dispatcher.registerEventListener).toHaveBeenCalledWith( - `${mockFrameId}.${eventType}`, - callback - ); - }); - }); - - it("calls unregisterEventListener on the dispatcher when off is called", () => { - return import("./feature").then(({ getFeature }) => { - const callback = jest.fn(); - const eventType = "treatmentPlan.activated"; - dispatcher.unregisterEventListener = jest.fn(); - const feature = getFeature( - mockFeatureType, - mockFeatureOptions, - mockFullscriptOptions, - dispatcher - ); - - feature.off(eventType, callback); - expect(dispatcher.unregisterEventListener).toHaveBeenCalledWith( - `${mockFrameId}.${eventType}`, - callback - ); - }); - }); + const eventTypes: EventType[] = [ + "treatmentPlan.activated", + "patient.selected", + "order.checkoutStarted", + ]; + + it.each(eventTypes)( + "calls registerEventListener on the dispatcher when on is called with %s", + eventType => { + return import("./feature").then(({ getFeature }) => { + const callback = jest.fn(); + dispatcher.registerEventListener = jest.fn(); + const feature = getFeature( + mockFeatureType, + mockFeatureOptions, + mockFullscriptOptions, + dispatcher + ); + + feature.on(eventType, callback); + expect(dispatcher.registerEventListener).toHaveBeenCalledWith( + `${mockFrameId}.${eventType}`, + callback + ); + }); + } + ); + + it.each(eventTypes)( + "calls unregisterEventListener on the dispatcher when off is called with %s", + eventType => { + return import("./feature").then(({ getFeature }) => { + const callback = jest.fn(); + dispatcher.unregisterEventListener = jest.fn(); + const feature = getFeature( + mockFeatureType, + mockFeatureOptions, + mockFullscriptOptions, + dispatcher + ); + + feature.off(eventType, callback); + expect(dispatcher.unregisterEventListener).toHaveBeenCalledWith( + `${mockFrameId}.${eventType}`, + callback + ); + }); + } + ); }); }); diff --git a/src/feature/index.ts b/src/feature/index.ts index 0b8833c..665302a 100644 --- a/src/feature/index.ts +++ b/src/feature/index.ts @@ -7,6 +7,7 @@ export type { EventListenerPayload, TreatmentPlanPayload, PatientPayload, + OrderPayload, EventType, } from "./eventType"; diff --git a/src/fullscriptJsValidator/fullscriptJsValidator.spec.ts b/src/fullscriptJsValidator/fullscriptJsValidator.spec.ts index 2f663aa..3d3f346 100644 --- a/src/fullscriptJsValidator/fullscriptJsValidator.spec.ts +++ b/src/fullscriptJsValidator/fullscriptJsValidator.spec.ts @@ -72,11 +72,14 @@ describe("fullscriptValidator", () => { }); describe("validateFeatureEventType", () => { - it("does not throw an error if the featureEventType is valid", () => { - expect(() => { - validateFeatureEventType("treatmentPlan.activated"); - }).not.toThrow(); - }); + it.each(["treatmentPlan.activated", "patient.selected", "order.checkoutStarted"])( + "does not throw an error if the featureEventType is %s", + eventType => { + expect(() => { + validateFeatureEventType(eventType); + }).not.toThrow(); + } + ); it("throws an error if the featureEventType is not treatmentPlan or a string", () => { const invalidEventType = "cancelled"; diff --git a/src/index.ts b/src/index.ts index b9b22fd..5d0d401 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export type { EventListenerCallback, TreatmentPlanPayload, PatientPayload, + OrderPayload, PatientOptions, FeatureOptions, } from "./feature";