diff --git a/plugin-hrm-form/package-lock.json b/plugin-hrm-form/package-lock.json index fea8f7acd3..028c35f0cb 100644 --- a/plugin-hrm-form/package-lock.json +++ b/plugin-hrm-form/package-lock.json @@ -150,7 +150,7 @@ "license": "AGPL", "dependencies": { "@babel/runtime": "^7.28.6", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "devDependencies": { "@babel/preset-env": "^7.16.5", diff --git a/plugin-hrm-form/src/___tests__/components/CSAMReport/csamReportApi.test.ts b/plugin-hrm-form/src/___tests__/components/CSAMReport/csamReportApi.test.ts index c003afd425..0fab7a199d 100644 --- a/plugin-hrm-form/src/___tests__/components/CSAMReport/csamReportApi.test.ts +++ b/plugin-hrm-form/src/___tests__/components/CSAMReport/csamReportApi.test.ts @@ -36,12 +36,12 @@ import { } from '../../../states/csam-report/actions'; import { changeRoute } from '../../../states/routing/actions'; import { CSAMReportEntry } from '../../../types/types'; -import { reportToIWF, selfReportToIWF } from '../../../services/ServerlessService'; +import { reportToIWF, selfReportToIWF } from '../../../services/iwfService'; import { acknowledgeCSAMReport, createCSAMReport } from '../../../services/CSAMReportService'; import { addExternalReportEntry } from '../../../states/csam-report/existingContactExternalReport'; import { csamReportBase, namespace, routingBase } from '../../../states/storeNamespaces'; -jest.mock('../../../services/ServerlessService', () => ({ +jest.mock('../../../services/iwfService', () => ({ reportToIWF: jest.fn(), selfReportToIWF: jest.fn(), })); diff --git a/plugin-hrm-form/src/___tests__/services/iwfService.test.ts b/plugin-hrm-form/src/___tests__/services/iwfService.test.ts new file mode 100644 index 0000000000..b6c3f89a65 --- /dev/null +++ b/plugin-hrm-form/src/___tests__/services/iwfService.test.ts @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +/* eslint-disable camelcase */ +import { reportToIWF, selfReportToIWF } from '../../services/iwfService'; +import fetchProtectedApi from '../../services/fetchProtectedApi'; +import { getAseloFeatureFlags } from '../../hrmConfig'; + +jest.mock('../../services/fetchProtectedApi'); +jest.mock('../../hrmConfig'); + +const mockFetchProtectedApi = fetchProtectedApi as jest.MockedFunction; +const mockGetAseloFeatureFlags = getAseloFeatureFlags as jest.MockedFunction; + +const counselorForm = { + webAddress: 'http://example.com', + description: 'Test description', + anonymous: 'non-anonymous', + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', +}; + +const childForm = { + childAge: '13-15', + ageVerified: true, +}; + +beforeEach(() => { + mockFetchProtectedApi.mockClear(); + mockGetAseloFeatureFlags.mockClear(); +}); + +describe('reportToIWF', () => { + const serverlessResponse = { + 'IWFReportService1.0': { responseData: 'ref123', responseCode: '200', responseDescription: 'OK' }, + }; + const lambdaResponse = { status: 200, data: serverlessResponse }; + + describe('feature flag disabled (serverless)', () => { + beforeEach(() => { + mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: false } as any); + mockFetchProtectedApi.mockResolvedValue(serverlessResponse); + }); + + test('calls fetchProtectedApi with serverless endpoint', async () => { + await reportToIWF(counselorForm); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + '/reportToIWF', + expect.objectContaining({ Reported_URL: 'http://example.com' }), + { useTwilioLambda: false }, + ); + }); + + test('returns the serverless response directly', async () => { + const result = await reportToIWF(counselorForm); + expect(result).toStrictEqual(serverlessResponse); + }); + + test('sets Reporter_Anonymous to N for non-anonymous form', async () => { + await reportToIWF(counselorForm); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ Reporter_Anonymous: 'N' }), + expect.any(Object), + ); + }); + + test('sets Reporter_Anonymous to Y for anonymous form', async () => { + await reportToIWF({ ...counselorForm, anonymous: 'anonymous' }); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ Reporter_Anonymous: 'Y' }), + expect.any(Object), + ); + }); + }); + + describe('feature flag enabled (lambda)', () => { + beforeEach(() => { + mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: true } as any); + mockFetchProtectedApi.mockResolvedValue(lambdaResponse); + }); + + test('calls fetchProtectedApi with lambda endpoint', async () => { + await reportToIWF(counselorForm); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + '/integrations/iwf/reportToIWF', + expect.objectContaining({ Reported_URL: 'http://example.com' }), + { useTwilioLambda: true }, + ); + }); + + test('returns response.data (unwrapped from lambda wrapper)', async () => { + const result = await reportToIWF(counselorForm); + expect(result).toStrictEqual(serverlessResponse); + }); + }); +}); + +describe('selfReportToIWF', () => { + const caseNumber = 'case-001'; + const selfReportResponse = { reportUrl: 'http://iwf.example.com/report?t=token', status: 'OK' }; + + describe('feature flag disabled (serverless)', () => { + beforeEach(() => { + mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: false } as any); + mockFetchProtectedApi.mockResolvedValue(selfReportResponse); + }); + + test('calls fetchProtectedApi with serverless endpoint', async () => { + await selfReportToIWF(childForm, caseNumber); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + '/selfReportToIWF', + { user_age_range: '13-15', case_number: caseNumber }, + { useTwilioLambda: false }, + ); + }); + + test('returns the response', async () => { + const result = await selfReportToIWF(childForm, caseNumber); + expect(result).toStrictEqual(selfReportResponse); + }); + }); + + describe('feature flag enabled (lambda)', () => { + beforeEach(() => { + mockGetAseloFeatureFlags.mockReturnValue({ use_twilio_lambda_for_iwf_reporting: true } as any); + mockFetchProtectedApi.mockResolvedValue(selfReportResponse); + }); + + test('calls fetchProtectedApi with lambda endpoint', async () => { + await selfReportToIWF(childForm, caseNumber); + expect(mockFetchProtectedApi).toHaveBeenCalledWith( + '/integrations/iwf/selfReportToIWF', + { user_age_range: '13-15', case_number: caseNumber }, + { useTwilioLambda: true }, + ); + }); + + test('returns the response', async () => { + const result = await selfReportToIWF(childForm, caseNumber); + expect(result).toStrictEqual(selfReportResponse); + }); + }); +}); diff --git a/plugin-hrm-form/src/components/CSAMReport/csamReportApi.ts b/plugin-hrm-form/src/components/CSAMReport/csamReportApi.ts index fa2ed40c03..0375069795 100644 --- a/plugin-hrm-form/src/components/CSAMReport/csamReportApi.ts +++ b/plugin-hrm-form/src/components/CSAMReport/csamReportApi.ts @@ -32,7 +32,7 @@ import { } from '../../states/csam-report/types'; import { addExternalReportEntry } from '../../states/csam-report/existingContactExternalReport'; import { acknowledgeCSAMReport, createCSAMReport } from '../../services/CSAMReportService'; -import { reportToIWF, selfReportToIWF } from '../../services/ServerlessService'; +import { reportToIWF, selfReportToIWF } from '../../services/iwfService'; import { newCSAMReportActionForContact } from '../../states/csam-report/actions'; import { csamReportBase, namespace } from '../../states/storeNamespaces'; import { getCurrentTopmostRouteForTask } from '../../states/routing/getRoute'; diff --git a/plugin-hrm-form/src/services/ServerlessService.ts b/plugin-hrm-form/src/services/ServerlessService.ts index c823298575..1e705c97e8 100644 --- a/plugin-hrm-form/src/services/ServerlessService.ts +++ b/plugin-hrm-form/src/services/ServerlessService.ts @@ -24,7 +24,6 @@ import { ITask, Notifications } from '@twilio/flex-ui'; import { DefinitionVersion, loadDefinition } from 'hrm-form-definitions'; import fetchProtectedApi from './fetchProtectedApi'; -import type { ChildCSAMReportForm, CounselorCSAMReportForm } from '../states/csam-report/types'; import { getHrmConfig } from '../hrmConfig'; type TransferChatStartBody = { @@ -87,39 +86,12 @@ export const getExternalRecordingS3Location = async (callSid: string) => { return response; }; -/** - * Send a CSAM report to IWF - */ -export const reportToIWF = async (form: CounselorCSAMReportForm) => { - const body = { - Reported_URL: form.webAddress, - Reporter_Description: form.description, - Reporter_Anonymous: form.anonymous === 'anonymous' ? 'Y' : 'N', - Reporter_First_Name: form.firstName, - Reporter_Last_Name: form.lastName, - Reporter_Email_ID: form.email, - }; - - const response = await fetchProtectedApi('/reportToIWF', body); - return response; -}; - export const saveContactToSaferNet = async (payload: any): Promise => { const body = { payload: JSON.stringify(payload) }; const postSurveyUrl = await fetchProtectedApi('/saveContactToSaferNet', body); return postSurveyUrl; }; -export const selfReportToIWF = async (form: ChildCSAMReportForm, caseNumber: string) => { - const body = { - user_age_range: form.childAge, - case_number: caseNumber, - }; - - const response = await fetchProtectedApi('/selfReportToIWF', body); - return response; -}; - export const getMediaUrl = async (serviceSid: string, mediaSid: string) => { const body = { serviceSid, mediaSid }; diff --git a/plugin-hrm-form/src/services/iwfService.ts b/plugin-hrm-form/src/services/iwfService.ts new file mode 100644 index 0000000000..42ab12f669 --- /dev/null +++ b/plugin-hrm-form/src/services/iwfService.ts @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +/* eslint-disable camelcase */ +import fetchProtectedApi from './fetchProtectedApi'; +import { getAseloFeatureFlags } from '../hrmConfig'; +import type { ChildCSAMReportForm, CounselorCSAMReportForm } from '../states/csam-report/types'; + +/** + * Send a CSAM report to IWF + */ +export const reportToIWF = async (form: CounselorCSAMReportForm) => { + const useTwilioLambda = getAseloFeatureFlags().use_twilio_lambda_for_iwf_reporting; + const body = { + Reported_URL: form.webAddress, + Reporter_Description: form.description, + Reporter_Anonymous: form.anonymous === 'anonymous' ? 'Y' : 'N', + Reporter_First_Name: form.firstName, + Reporter_Last_Name: form.lastName, + Reporter_Email_ID: form.email, + }; + + const response = await fetchProtectedApi(useTwilioLambda ? '/integrations/iwf/reportToIWF' : '/reportToIWF', body, { + useTwilioLambda, + }); + // The account-scoped lambda wraps the IWF response in { status, data }, whereas the + // serverless endpoint returns the IWF response directly. Normalise to the direct format. + return useTwilioLambda ? response.data : response; +}; + +export const selfReportToIWF = async (form: ChildCSAMReportForm, caseNumber: string) => { + const useTwilioLambda = getAseloFeatureFlags().use_twilio_lambda_for_iwf_reporting; + const body = { + user_age_range: form.childAge, + case_number: caseNumber, + }; + + // The account-scoped lambda returns { reportUrl, status } directly (no wrapper), matching the + // serverless response format, so no normalisation is required here. + return fetchProtectedApi(useTwilioLambda ? '/integrations/iwf/selfReportToIWF' : '/selfReportToIWF', body, { + useTwilioLambda, + }); +}; diff --git a/plugin-hrm-form/src/types/FeatureFlags.ts b/plugin-hrm-form/src/types/FeatureFlags.ts index 2a2f9764bc..d6a045b69e 100644 --- a/plugin-hrm-form/src/types/FeatureFlags.ts +++ b/plugin-hrm-form/src/types/FeatureFlags.ts @@ -47,5 +47,6 @@ export type FeatureFlags = { use_prepopulate_mappings: boolean; // Use PrepopulateMappings.json instead of PrepopulateKeys.json use_twilio_lambda_for_conference_functions: boolean; // Use the twilio account scoped lambda for conferencing functions use_twilio_lambda_for_conversation_duration: boolean; // Use the twilio account scoped lambda to calculate conversationDuration + use_twilio_lambda_for_iwf_reporting: boolean; // Use the twilio account scoped lambda for reportToIWF and selfReportToIWF use_twilio_lambda_for_task_assignment: boolean; // Use the twilio account scoped lambda for getTasksAndReservations, checkTaskAssignment, completeTaskAssignment };