diff --git a/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts b/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts index db8d3bd75..865171223 100644 --- a/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts +++ b/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts @@ -242,7 +242,7 @@ describe("Unit tests for statusUpdate", function () { const statusUpdates = simpleStatusUpdatesPayload() - const responseBundle = JSON.parse(JSON.stringify(requestBundle)) + const responseBundle = structuredClone(requestBundle) applyStatusUpdates(logger, requestBundle, statusUpdates) @@ -410,7 +410,7 @@ describe("Unit tests for statusUpdate", function () { const statusUpdateRequest = createStatusUpdateRequest([{odsCode: "FLM49", prescriptionID: prescriptionID}]) applyTemporaryStatusUpdates(logger, requestBundle, statusUpdateRequest) - const statusExtension = medicationRequest.extension![0].extension!.filter((e) => e.url === "status")[0] + const statusExtension = medicationRequest.extension![0].extension!.find((e) => e.url === "status")! expect(statusExtension.valueCoding!.code!).toEqual(TEMPORARILY_UNAVAILABLE_STATUS) expect(medicationRequest.status).toEqual("active") @@ -451,7 +451,7 @@ describe("Unit tests for statusUpdate", function () { const statusUpdateRequest = createStatusUpdateRequest([{odsCode: "FLM49", prescriptionID: prescriptionID}]) applyTemporaryStatusUpdates(logger, requestBundle, statusUpdateRequest) - const statusExtension = medicationRequest.extension![0].extension!.filter((e) => e.url === "status")[0] + const statusExtension = medicationRequest.extension![0].extension!.find((e) => e.url === "status")! expect(statusExtension.valueCoding!.code!).toEqual(shouldUpdate ? TEMPORARILY_UNAVAILABLE_STATUS : status) } @@ -487,10 +487,10 @@ describe("Unit tests for statusUpdate", function () { applyTemporaryStatusUpdates(logger, requestBundle, statusUpdateRequest) const tempStatusUpdateFilter = (medicationRequest: MedicationRequest) => { - const outerExtension = medicationRequest.extension?.filter( + const outerExtension = medicationRequest.extension?.find( (extension) => extension.url === OUTER_EXTENSION_URL - )[0] - const statusExtension = outerExtension?.extension?.filter((extension) => extension.url === "status")[0] + ) + const statusExtension = outerExtension?.extension?.find((extension) => extension.url === "status") return statusExtension?.valueCoding!.code === TEMPORARILY_UNAVAILABLE_STATUS } diff --git a/packages/getMyPrescriptions/src/getMyPrescriptions.ts b/packages/getMyPrescriptions/src/getMyPrescriptions.ts index 51aae40d0..fe996f484 100644 --- a/packages/getMyPrescriptions/src/getMyPrescriptions.ts +++ b/packages/getMyPrescriptions/src/getMyPrescriptions.ts @@ -24,12 +24,7 @@ import { ResponseFunc } from "./responses" import {extractNHSNumberFromHeaders, NHSNumberValidationError, validateNHSNumber} from "./extractNHSNumber" -import { - deepCopy, - hasTimedOut, - jobWithTimeout, - NHS_LOGIN_HEADER -} from "./utils" +import {hasTimedOut, jobWithTimeout, NHS_LOGIN_HEADER} from "./utils" import {buildStatusUpdateData, shouldGetStatusUpdates} from "./statusUpdate" import {extractOdsCodes, isolateOperationOutcome} from "./fhirUtils" import {pfpConfig, PfPConfig} from "@pfp-common/utilities" @@ -135,7 +130,7 @@ async function eventHandler( const statusUpdateData = includeStatusUpdateData ? buildStatusUpdateData(logger, searchsetBundle) : undefined const distanceSelling = new DistanceSelling(servicesCache, logger) - const distanceSellingBundle = deepCopy(searchsetBundle) + const distanceSellingBundle = structuredClone(searchsetBundle) const distanceSellingCallout = distanceSelling.search(distanceSellingBundle) const distanceSellingResponse = await jobWithTimeout(params.serviceSearchTimeoutMs, distanceSellingCallout) @@ -189,7 +184,7 @@ export function adaptHeadersToSpine(headers: EventHeaders): EventHeaders { if (!subjectNHSNumber) { throw new NHSNumberValidationError(`${DELEGATED_ACCESS_SUB_HDR} header not present for delegated access`) } - if (subjectNHSNumber.indexOf(":") > -1) { + if (subjectNHSNumber.includes(":")) { logger.warn(`${DELEGATED_ACCESS_SUB_HDR} is not expected to be prefixed by proofing level, but is, removing it`) subjectNHSNumber = subjectNHSNumber.split(":")[1] } diff --git a/packages/getMyPrescriptions/src/utils.ts b/packages/getMyPrescriptions/src/utils.ts index 14a54b98a..8f24494d9 100644 --- a/packages/getMyPrescriptions/src/utils.ts +++ b/packages/getMyPrescriptions/src/utils.ts @@ -1,7 +1,3 @@ -export function deepCopy(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - export interface Timeout { isTimeout: true } diff --git a/packages/getMyPrescriptions/tests/statusUpdate.test.ts b/packages/getMyPrescriptions/tests/statusUpdate.test.ts index 6d7365f93..1d195a81e 100644 --- a/packages/getMyPrescriptions/tests/statusUpdate.test.ts +++ b/packages/getMyPrescriptions/tests/statusUpdate.test.ts @@ -7,18 +7,29 @@ import { jest } from "@jest/globals" import axios from "axios" +import MockAdapter from "axios-mock-adapter" import {Bundle, MedicationRequest} from "fhir/r4" import {APIGatewayProxyResult as LambdaResult, Context} from "aws-lambda" -import MockAdapter from "axios-mock-adapter" +import {LogLevel} from "@aws-lambda-powertools/logger/types" +import {Logger} from "@aws-lambda-powertools/logger" +import {createSpineClient} from "@NHSDigital/eps-spine-client" +import {MiddyfiedHandler} from "@middy/core" import { + createMockedPfPConfig, helloworldContext, mockAPIResponseBody as mockResponseBody, mockInteractionResponseBody, mockPharmacy2uResponse, mockPharmicaResponse, - mockStateMachineInputEvent + mockStateMachineInputEvent, + MockedPfPConfig, + setupTestEnvironment } from "@pfp-common/testing" +import { + SERVICE_SEARCH_BASE_QUERY_PARAMS, + getServiceSearchEndpoint +} from "@prescriptionsforpatients/serviceSearchClient" import {buildStatusUpdateData} from "../src/statusUpdate" import {StateMachineFunctionResponseBody} from "../src/responses" @@ -29,13 +40,7 @@ import { newHandler, stateMachineEventHandler } from "../src/getMyPrescriptions" -import {EXPECTED_TRACE_IDS, SERVICE_SEARCH_PARAMS} from "./utils" -import {createMockedPfPConfig, setupTestEnvironment} from "@pfp-common/testing" -import type {MockedPfPConfig} from "@pfp-common/testing" -import {LogLevel} from "@aws-lambda-powertools/logger/types" -import {Logger} from "@aws-lambda-powertools/logger" -import {createSpineClient} from "@NHSDigital/eps-spine-client" -import {MiddyfiedHandler} from "@middy/core" +import {EXPECTED_TRACE_IDS} from "./utils" const exampleEvent = JSON.stringify(mockStateMachineInputEvent) const exampleInteractionResponse = JSON.stringify(mockInteractionResponseBody) @@ -144,10 +149,10 @@ describe("Unit tests for statusUpdate, via handler", function () { const event: GetMyPrescriptionsEvent = JSON.parse(exampleEvent) mock - .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, JSON.parse(exampleInteractionResponse)) diff --git a/packages/getMyPrescriptions/tests/test-handler.test.ts b/packages/getMyPrescriptions/tests/test-handler.test.ts index 728e6aa7a..8ad14bb6c 100644 --- a/packages/getMyPrescriptions/tests/test-handler.test.ts +++ b/packages/getMyPrescriptions/tests/test-handler.test.ts @@ -1,12 +1,6 @@ import {APIGatewayProxyResult as LambdaResult, Context} from "aws-lambda" -import { - DEFAULT_HANDLER_PARAMS, - newHandler, - GetMyPrescriptionsEvent, - stateMachineEventHandler, - STATE_MACHINE_MIDDLEWARE -} from "../src/getMyPrescriptions" import {Logger} from "@aws-lambda-powertools/logger" +import {LogLevel} from "@aws-lambda-powertools/logger/types" import axios from "axios" import MockAdapter from "axios-mock-adapter" import { @@ -15,24 +9,35 @@ import { it, jest } from "@jest/globals" +import {createSpineClient} from "@NHSDigital/eps-spine-client" +import {MiddyfiedHandler} from "@middy/core" import { + createMockedPfPConfig, mockAPIResponseBody as mockResponseBody, mockInteractionResponseBody, mockPharmacy2uResponse, mockPharmicaResponse, helloworldContext, - mockStateMachineInputEvent + mockStateMachineInputEvent, + MockedPfPConfig, + setupTestEnvironment } from "@pfp-common/testing" +import { + SERVICE_SEARCH_BASE_QUERY_PARAMS, + getServiceSearchEndpoint +} from "@prescriptionsforpatients/serviceSearchClient" +import { + DEFAULT_HANDLER_PARAMS, + newHandler, + GetMyPrescriptionsEvent, + stateMachineEventHandler, + STATE_MACHINE_MIDDLEWARE +} from "../src/getMyPrescriptions" import {HEADERS, StateMachineFunctionResponseBody, TIMEOUT_RESPONSE} from "../src/responses" import "./toMatchJsonLogMessage" import {EXPECTED_TRACE_IDS} from "./utils" -import {LogLevel} from "@aws-lambda-powertools/logger/types" -import {createSpineClient} from "@NHSDigital/eps-spine-client" -import {MiddyfiedHandler} from "@middy/core" -import {createMockedPfPConfig, setupTestEnvironment} from "@pfp-common/testing" -import type {MockedPfPConfig} from "@pfp-common/testing" const TC008_NHS_NUMBER = "9992387920" @@ -356,14 +361,6 @@ describe("Unit tests for app handler including service search", function () { let testEnv: ReturnType let mockedConfig: MockedPfPConfig - const queryParams = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 - } - beforeEach(() => { testEnv = setupTestEnvironment() mockedConfig = createMockedPfPConfig([TC008_NHS_NUMBER]) @@ -395,11 +392,11 @@ describe("Unit tests for app handler including service search", function () { const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, JSON.parse(exampleInteractionResponse)) @@ -435,11 +432,11 @@ describe("Unit tests for app handler including service search", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, interactionResponse) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) @@ -466,7 +463,7 @@ describe("Unit tests for app handler including service search", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, exampleResponse) // eslint-disable-next-line @typescript-eslint/no-unused-vars - mock.onGet("https://service-search/service-search").reply(function (config) { + mock.onGet(getServiceSearchEndpoint()).reply(function (config) { return new Promise((resolve) => setTimeout(() => resolve([200, {}]), 15_000)) }) @@ -503,14 +500,6 @@ describe("Unit tests for logging functionality", function () { let testEnv: ReturnType let mockedConfig: MockedPfPConfig - const queryParams = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 - } - beforeEach(() => { testEnv = setupTestEnvironment() mockedConfig = createMockedPfPConfig([TC008_NHS_NUMBER]) @@ -591,11 +580,11 @@ describe("Unit tests for logging functionality", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, interactionResponse) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) diff --git a/packages/getMyPrescriptions/tests/utils.ts b/packages/getMyPrescriptions/tests/utils.ts index 32c784675..61b9ef8cd 100644 --- a/packages/getMyPrescriptions/tests/utils.ts +++ b/packages/getMyPrescriptions/tests/utils.ts @@ -12,14 +12,6 @@ export function mockInternalDependency(modulePath: string, module: object, depen return mockDependency } -export const SERVICE_SEARCH_PARAMS = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 -} - export const EXPECTED_TRACE_IDS: TraceIDs = { "apigw-request-id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "nhsd-correlation-id": "test-request-id.test-correlation-id.rrt-5789322914740101037-b-aet2-20145-482635-2", diff --git a/packages/serviceSearchClient/src/index.ts b/packages/serviceSearchClient/src/index.ts index d0875767b..a5e903bd4 100644 --- a/packages/serviceSearchClient/src/index.ts +++ b/packages/serviceSearchClient/src/index.ts @@ -1,3 +1,2 @@ -import {createServiceSearchClient, ServiceSearchClient} from "./serviceSearch-client" - -export {createServiceSearchClient, ServiceSearchClient} +export {createServiceSearchClient, ServiceSearchClient} from "./serviceSearch-client" +export {SERVICE_SEARCH_BASE_QUERY_PARAMS, getServiceSearchEndpoint} from "./live-serviceSearch-client" diff --git a/packages/serviceSearchClient/src/live-serviceSearch-client.ts b/packages/serviceSearchClient/src/live-serviceSearch-client.ts index 536a806a7..0a6f35084 100644 --- a/packages/serviceSearchClient/src/live-serviceSearch-client.ts +++ b/packages/serviceSearchClient/src/live-serviceSearch-client.ts @@ -18,19 +18,30 @@ export type ServiceSearchData = { "value": Array } +export const SERVICE_SEARCH_BASE_QUERY_PARAMS = { + "api-version": 2, + "searchFields": "ODSCode", + "$filter": "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", + "$select": "URL,OrganisationSubType", + "$top": 1 +} + +export function getServiceSearchEndpoint(targetServer?: string): string { + const endpoint = targetServer || process.env.TargetServiceSearchServer || "service-search" + const baseUrl = `https://${endpoint}` + if (endpoint.toLowerCase().includes("api.service.nhs.uk")) { + // service search v3 + SERVICE_SEARCH_BASE_QUERY_PARAMS["api-version"] = 3 + return `${baseUrl}/service-search-api/` + } + // service search v2 + return `${baseUrl}/service-search` +} + export class LiveServiceSearchClient implements ServiceSearchClient { - private readonly SERVICE_SEARCH_URL_SCHEME = "https" - private readonly SERVICE_SEARCH_ENDPOINT = process.env.TargetServiceSearchServer private readonly axiosInstance: AxiosInstance private readonly logger: Logger - private readonly outboundHeaders: {"Subscription-Key": string | undefined} - private readonly baseQueryParams: { - "api-version": number, - "searchFields": string, - "$filter": string, - "$select": string, - "$top": number - } + private readonly outboundHeaders: {"apikey": string | undefined, "Subscription-Key": string | undefined} constructor(logger: Logger) { this.logger = logger @@ -39,17 +50,17 @@ export class LiveServiceSearchClient implements ServiceSearchClient { axiosRetry(this.axiosInstance, {retries: 3}) this.axiosInstance.interceptors.request.use((config) => { - config.headers["request-startTime"] = new Date().getTime() + config.headers["request-startTime"] = Date.now() return config }) this.axiosInstance.interceptors.response.use((response) => { - const currentTime = new Date().getTime() + const currentTime = Date.now() const startTime = response.config.headers["request-startTime"] this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime}) return response }, (error) => { - const currentTime = new Date().getTime() + const currentTime = Date.now() const startTime = error.config?.headers["request-startTime"] this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime}) @@ -85,21 +96,15 @@ export class LiveServiceSearchClient implements ServiceSearchClient { }) this.outboundHeaders = { - "Subscription-Key": process.env.ServiceSearchApiKey - } - this.baseQueryParams = { - "api-version": 2, - "searchFields": "ODSCode", - "$filter": "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - "$select": "URL,OrganisationSubType", - "$top": 1 + "Subscription-Key": process.env.ServiceSearchApiKey, + "apikey": process.env.ServiceSearch3ApiKey } } async searchService(odsCode: string): Promise { try { - const address = this.getServiceSearchEndpoint() - const queryParams = {...this.baseQueryParams, search: odsCode} + const address = getServiceSearchEndpoint() + const queryParams = {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: odsCode} this.logger.info(`making request to ${address} with ods code ${odsCode}`, {odsCode: odsCode}) const response = await this.axiosInstance.get(address, { @@ -154,16 +159,14 @@ export class LiveServiceSearchClient implements ServiceSearchClient { } stripApiKeyFromHeaders(error: AxiosError) { - const headerKey = "subscription-key" - if (error.response?.headers) { - delete error.response.headers[headerKey] - } - if (error.request?.headers) { - delete error.request.headers[headerKey] - } - } - - private getServiceSearchEndpoint() { - return `${this.SERVICE_SEARCH_URL_SCHEME}://${this.SERVICE_SEARCH_ENDPOINT}/service-search` + const headerKeys = ["subscription-key", "apikey"] + headerKeys.forEach((key) => { + if (error.response?.headers) { + delete error.response.headers[key] + } + if (error.request?.headers) { + delete error.request.headers[key] + } + }) } } diff --git a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts index cc8219193..38f08b620 100644 --- a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts +++ b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts @@ -11,7 +11,7 @@ process.env.TargetServiceSearchServer = "live" process.env.ServiceSearchApiKey = "test-key" const serviceSearchUrl = "https://live/service-search" -type ServiceSearchTestData = { +interface ServiceSearchTestData { scenarioDescription: string serviceSearchData: ServiceSearchData expected: URL | undefined @@ -28,9 +28,10 @@ describe("live serviceSearch client", () => { jest.restoreAllMocks() }) - // Private helper tests - test("getServiceSearchEndpoint returns correct URL", () => { - const endpoint = client["getServiceSearchEndpoint"]() + // Helper function tests + test("getServiceSearchEndpoint returns correct URL", async () => { + const {getServiceSearchEndpoint} = await import("../src/live-serviceSearch-client.js") + const endpoint = getServiceSearchEndpoint() expect(endpoint).toBe(serviceSearchUrl) })