diff --git a/frontend/src/__tests__/app/choose-templates/page.test.tsx b/frontend/src/__tests__/app/choose-templates/page.test.tsx index 02c0504c1..e69708789 100644 --- a/frontend/src/__tests__/app/choose-templates/page.test.tsx +++ b/frontend/src/__tests__/app/choose-templates/page.test.tsx @@ -7,10 +7,7 @@ import { getRoutingConfig, getMessagePlanTemplates, } from '@utils/message-plans'; -import type { - RoutingConfig, - RoutingConfigStatus, -} from 'nhs-notify-backend-client'; +import type { RoutingConfig } from 'nhs-notify-backend-client'; import { EMAIL_TEMPLATE, LETTER_TEMPLATE, @@ -34,13 +31,14 @@ const validRoutingConfigId = 'fbb81055-79b9-4759-ac07-d191ae57be34'; const routingConfig: RoutingConfig = { id: validRoutingConfigId, name: 'Autumn Campaign Plan', - status: 'DRAFT' as RoutingConfigStatus, + status: 'DRAFT', clientId: 'client-1', campaignId: 'campaign-2', createdAt: '2025-01-13T10:19:25.579Z', updatedAt: '2025-01-13T10:19:25.579Z', cascadeGroupOverrides: [], cascade: [], + defaultCascadeGroup: 'standard', }; describe('ChooseTemplatesPage', () => { diff --git a/frontend/src/__tests__/app/message-plans/create-message-plan/server-action.test.ts b/frontend/src/__tests__/app/message-plans/create-message-plan/server-action.test.ts index 776db6fd0..6ab71a15f 100644 --- a/frontend/src/__tests__/app/message-plans/create-message-plan/server-action.test.ts +++ b/frontend/src/__tests__/app/message-plans/create-message-plan/server-action.test.ts @@ -1,10 +1,6 @@ import { mock } from 'jest-mock-extended'; import { redirect, RedirectType } from 'next/navigation'; -import type { - CascadeGroup, - CascadeItem, - RoutingConfig, -} from 'nhs-notify-backend-client'; +import type { CascadeItem, RoutingConfig } from 'nhs-notify-backend-client'; import type { MessageOrder } from 'nhs-notify-web-template-management-utils'; import { createMessagePlanServerAction } from '@app/message-plans/create-message-plan/server-action'; import { NextRedirectError } from '@testhelpers/next-redirect'; @@ -26,175 +22,166 @@ beforeEach(() => { jest.clearAllMocks(); }); -const MESSAGE_ORDER_SCENARIOS: [MessageOrder, CascadeItem[], CascadeGroup[]][] = +const MESSAGE_ORDER_SCENARIOS: [MessageOrder, CascadeItem[]][] = [ [ + 'NHSAPP', [ - 'NHSAPP', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,EMAIL', [ - 'NHSAPP,EMAIL', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'EMAIL', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,SMS', [ - 'NHSAPP,SMS', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,EMAIL,SMS', [ - 'NHSAPP,EMAIL,SMS', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'EMAIL', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,SMS,EMAIL', [ - 'NHSAPP,SMS,EMAIL', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'EMAIL', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,SMS,LETTER', [ - 'NHSAPP,SMS,LETTER', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'LETTER', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'NHSAPP,EMAIL,SMS,LETTER', [ - 'NHSAPP,EMAIL,SMS,LETTER', - [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'EMAIL', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: null, - }, - { - cascadeGroups: ['standard'], - channel: 'LETTER', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + }, + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + }, ], + ], + [ + 'LETTER', [ - 'LETTER', - [ - { - cascadeGroups: ['standard'], - channel: 'LETTER', - channelType: 'primary', - defaultTemplateId: null, - }, - ], - [{ name: 'standard' }], + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + }, ], - ]; + ], +]; test.each(MESSAGE_ORDER_SCENARIOS)( 'creates a message plan with correct initial cascade for %s message order and redirects to the choose templates page', - async (messageOrder, expectedCascade, expectedCascadeGroups) => { + async (messageOrder, expectedCascade) => { const form = new FormData(); form.append('name', 'Message Plan Name'); form.append('campaignId', 'test-campaign-id'); @@ -212,7 +199,7 @@ test.each(MESSAGE_ORDER_SCENARIOS)( name: 'Message Plan Name', campaignId: 'test-campaign-id', cascade: expectedCascade, - cascadeGroupOverrides: expectedCascadeGroups, + cascadeGroupOverrides: [], }); } ); diff --git a/frontend/src/__tests__/app/message-plans/edit-message-plan-settings/page.test.tsx b/frontend/src/__tests__/app/message-plans/edit-message-plan-settings/page.test.tsx index eb80ab685..0bd2dffd3 100644 --- a/frontend/src/__tests__/app/message-plans/edit-message-plan-settings/page.test.tsx +++ b/frontend/src/__tests__/app/message-plans/edit-message-plan-settings/page.test.tsx @@ -132,7 +132,7 @@ describe('single campaign', () => { }); it('updates the message plan and redirects to the choose templates page', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); const page = await EditMessagePlanPage({ params: Promise.resolve({ routingConfigId: routingConfig.id }), @@ -142,7 +142,7 @@ describe('single campaign', () => { await user.clear(await screen.findByTestId('name-field')); - await user.click(await screen.getByTestId('name-field')); + await user.click(screen.getByTestId('name-field')); await user.keyboard('New Name'); @@ -230,7 +230,7 @@ describe('multiple campaigns', () => { }); it('updates the message plan and redirects to the choose templates page', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); const page = await EditMessagePlanPage({ params: Promise.resolve({ routingConfigId: routingConfig.id }), @@ -240,7 +240,7 @@ describe('multiple campaigns', () => { await user.clear(await screen.findByTestId('name-field')); - await user.click(await screen.getByTestId('name-field')); + await user.click(screen.getByTestId('name-field')); await user.keyboard('New Name'); diff --git a/frontend/src/__tests__/app/message-plans/page.test.tsx b/frontend/src/__tests__/app/message-plans/page.test.tsx index 48c1c3a5f..46775025a 100644 --- a/frontend/src/__tests__/app/message-plans/page.test.tsx +++ b/frontend/src/__tests__/app/message-plans/page.test.tsx @@ -24,6 +24,7 @@ const buildRoutingConfig = (rc: Partial): RoutingConfig => ({ cascadeGroupOverrides: [], clientId: 'client-a', createdAt: '2025-09-09T10:00:00Z', + defaultCascadeGroup: 'standard', status: 'DRAFT', id: '', name: '', diff --git a/frontend/src/__tests__/components/forms/ChooseChannelTemplate/server-action.test.ts b/frontend/src/__tests__/components/forms/ChooseChannelTemplate/server-action.test.ts index cedfc3054..21c00b419 100644 --- a/frontend/src/__tests__/components/forms/ChooseChannelTemplate/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/ChooseChannelTemplate/server-action.test.ts @@ -5,6 +5,7 @@ import { NHS_APP_TEMPLATE, ROUTING_CONFIG, SMS_TEMPLATE, + LETTER_TEMPLATE, } from '@testhelpers/helpers'; import { updateRoutingConfig } from '@utils/message-plans'; import { redirect, RedirectType } from 'next/navigation'; @@ -102,3 +103,55 @@ test('submit form - success updates config and redirects to choose templates', a RedirectType.push ); }); + +test('submit form - success updates config and redirects to choose templates for letter template with supplier references', async () => { + const mockRedirect = jest.mocked(redirect); + const mockUpdateRoutingConfig = jest.mocked(updateRoutingConfig); + + await chooseChannelTemplateAction( + { + messagePlan: { + ...ROUTING_CONFIG, + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: 'letter-template-id', + }, + ], + }, + pageHeading: 'Choose an email template', + templateList: [ + { + ...LETTER_TEMPLATE, + supplierReferences: { + MBA: 'mba-supplier-reference', + }, + }, + ], + cascadeIndex: 0, + }, + getMockFormData({ + channelTemplate: LETTER_TEMPLATE.id, + }) + ); + + expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(ROUTING_CONFIG.id, { + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: LETTER_TEMPLATE.id, + supplierReferences: { MBA: 'mba-supplier-reference' }, + }, + ], + cascadeGroupOverrides: [], + }); + + expect(mockRedirect).toHaveBeenCalledWith( + `/message-plans/choose-templates/${ROUTING_CONFIG.id}`, + RedirectType.push + ); +}); diff --git a/frontend/src/__tests__/components/organisms/CreateEditMessagePlan.test.tsx b/frontend/src/__tests__/components/organisms/CreateEditMessagePlan.test.tsx index aab1a9272..4835d18d8 100644 --- a/frontend/src/__tests__/components/organisms/CreateEditMessagePlan.test.tsx +++ b/frontend/src/__tests__/components/organisms/CreateEditMessagePlan.test.tsx @@ -37,6 +37,7 @@ function buildRoutingConfig({ channelType: 'primary', defaultTemplateId: `template-${index}`, })), + defaultCascadeGroup: 'standard', }; } diff --git a/frontend/src/__tests__/components/organisms/MessagePlanChannelList.test.tsx b/frontend/src/__tests__/components/organisms/MessagePlanChannelList.test.tsx index 3ab117f61..a2126358a 100644 --- a/frontend/src/__tests__/components/organisms/MessagePlanChannelList.test.tsx +++ b/frontend/src/__tests__/components/organisms/MessagePlanChannelList.test.tsx @@ -25,6 +25,7 @@ function buildRoutingConfig(channels: Channel[]): RoutingConfig { channelType: 'primary', defaultTemplateId: `test-template-${i}`, })), + defaultCascadeGroup: 'standard', }; } diff --git a/frontend/src/__tests__/helpers/helpers.ts b/frontend/src/__tests__/helpers/helpers.ts index ae9736c2d..a35f338ab 100644 --- a/frontend/src/__tests__/helpers/helpers.ts +++ b/frontend/src/__tests__/helpers/helpers.ts @@ -1,5 +1,6 @@ import { mockDeep } from 'jest-mock-extended'; import { RoutingConfig, TemplateDto } from 'nhs-notify-backend-client'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; function* iteratorFromList(list: T[]): IterableIterator { for (const item of list) { @@ -51,7 +52,7 @@ export const SMS_TEMPLATE: TemplateDto = { lockNumber: 1, } as const; -export const LETTER_TEMPLATE: TemplateDto = { +export const LETTER_TEMPLATE: LetterTemplate = { id: 'letter-template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', @@ -105,4 +106,5 @@ export const ROUTING_CONFIG: RoutingConfig = { defaultTemplateId: LETTER_TEMPLATE.id, }, ], + defaultCascadeGroup: 'standard', }; diff --git a/frontend/src/__tests__/helpers/routing-config-factory.ts b/frontend/src/__tests__/helpers/routing-config-factory.ts index 20c537b8e..f1f6e1127 100644 --- a/frontend/src/__tests__/helpers/routing-config-factory.ts +++ b/frontend/src/__tests__/helpers/routing-config-factory.ts @@ -10,6 +10,7 @@ export const RoutingConfigFactory = { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), campaignId: randomUUID(), + defaultCascadeGroup: 'standard', cascade: [ { cascadeGroups: ['standard'], diff --git a/frontend/src/__tests__/utils/form-actions.test.ts b/frontend/src/__tests__/utils/form-actions.test.ts index ebbd8c183..f37d87bf8 100644 --- a/frontend/src/__tests__/utils/form-actions.test.ts +++ b/frontend/src/__tests__/utils/form-actions.test.ts @@ -773,6 +773,7 @@ describe('form-actions', () => { status: 'DRAFT', updatedAt: now.toISOString(), lockNumber: 1, + defaultCascadeGroup: 'standard', }, }) ); @@ -788,7 +789,7 @@ describe('form-actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }); expect(mockedRoutingConfigClient.create).toHaveBeenCalledWith( @@ -803,7 +804,7 @@ describe('form-actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }, 'token' ); @@ -825,7 +826,8 @@ describe('form-actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', }); }); @@ -848,7 +850,7 @@ describe('form-actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }) ).rejects.toThrow('Failed to get access token'); @@ -877,7 +879,7 @@ describe('form-actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }) ).rejects.toThrow('Failed to create message plan'); }); diff --git a/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts b/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts index 1ce2e709d..5dd7bc155 100644 --- a/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts +++ b/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts @@ -10,7 +10,8 @@ const baseConfig: RoutingConfig = { createdAt: '2025-01-01T00:00:00.000Z', updatedAt: '2025-01-01T00:00:00.000Z', cascade: [], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', }; describe('getMessagePlanTemplateIds', () => { diff --git a/frontend/src/__tests__/utils/message-plans.test.ts b/frontend/src/__tests__/utils/message-plans.test.ts index 5fa9089c3..3642e5f9a 100644 --- a/frontend/src/__tests__/utils/message-plans.test.ts +++ b/frontend/src/__tests__/utils/message-plans.test.ts @@ -66,7 +66,8 @@ const baseConfig: RoutingConfig = { createdAt: '2025-01-01T00:00:00.000Z', updatedAt: '2025-01-01T00:00:00.000Z', cascade: [validCascadeItem], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', }; describe('Message plans actions', () => { @@ -116,7 +117,8 @@ describe('Message plans actions', () => { cascadeGroups: ['standard'], }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', } satisfies Omit; const routingConfigs = [ @@ -193,9 +195,10 @@ describe('Message plans actions', () => { campaignId: 'campaignId', clientId: 'clientId', cascade: [], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], id: 'a487ed49-e2f7-4871-ac8d-0c6c682c71f5', createdAt: '2022-01-01T00:00:00.000Z', + defaultCascadeGroup: 'standard', }, ], }); @@ -560,6 +563,7 @@ describe('Message plans actions', () => { id, clientId: 'client1', createdAt: now.toISOString(), + defaultCascadeGroup: 'standard', status: 'DRAFT', updatedAt: now.toISOString(), }, @@ -576,7 +580,7 @@ describe('Message plans actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }); expect(routingConfigApiMock.create).toHaveBeenCalledWith( @@ -591,7 +595,7 @@ describe('Message plans actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }, 'mock-token' ); @@ -612,7 +616,8 @@ describe('Message plans actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', }); }); @@ -635,7 +640,7 @@ describe('Message plans actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }) ).rejects.toThrow('Failed to get access token'); @@ -664,7 +669,7 @@ describe('Message plans actions', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }) ).rejects.toThrow('Failed to create message plan'); }); diff --git a/frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.test.ts b/frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.test.ts index 469774275..b3c52f48f 100644 --- a/frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.test.ts +++ b/frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.test.ts @@ -41,6 +41,7 @@ const baseConfig = { }, ], cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', }; describe('removeTemplateFromMessagePlan', () => { diff --git a/frontend/src/app/message-plans/create-message-plan/server-action.ts b/frontend/src/app/message-plans/create-message-plan/server-action.ts index ad44c4236..79dd3751b 100644 --- a/frontend/src/app/message-plans/create-message-plan/server-action.ts +++ b/frontend/src/app/message-plans/create-message-plan/server-action.ts @@ -2,11 +2,7 @@ import { redirect, RedirectType } from 'next/navigation'; import { z } from 'zod/v4'; -import type { - CascadeGroup, - CascadeItem, - Channel, -} from 'nhs-notify-backend-client'; +import type { CascadeItem, Channel } from 'nhs-notify-backend-client'; import { MESSAGE_ORDER_OPTIONS_LIST, type FormState, @@ -88,10 +84,6 @@ function messageOrderToInitialCascade( )[messageOrder]; } -function messageOrderToInitialCascadeGroups(_: MessageOrder): CascadeGroup[] { - return [{ name: 'standard' }]; -} - export async function createMessagePlanServerAction( formState: FormState, formData: FormData @@ -113,9 +105,7 @@ export async function createMessagePlanServerAction( name: parsed.data.name, campaignId: parsed.data.campaignId, cascade: messageOrderToInitialCascade(parsed.data.messageOrder), - cascadeGroupOverrides: messageOrderToInitialCascadeGroups( - parsed.data.messageOrder - ), + cascadeGroupOverrides: [], }); redirect(`/message-plans/choose-templates/${created.id}`, RedirectType.push); diff --git a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts index 212b1c209..03f980501 100644 --- a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts +++ b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts @@ -18,7 +18,7 @@ export async function chooseChannelTemplateAction( formState: ChooseChannelTemplateFormState, formData: FormData ): Promise { - const { messagePlan, cascadeIndex, pageHeading } = formState; + const { messagePlan, cascadeIndex, templateList, pageHeading } = formState; const parsedForm = $ChooseChannelTemplate(pageHeading).safeParse( Object.fromEntries(formData.entries()) @@ -31,9 +31,22 @@ export async function chooseChannelTemplateAction( }; } + const selectedTemplateId = parsedForm.data.channelTemplate; + + const selectedTemplate = templateList.find( + ({ id }) => id === selectedTemplateId + ); + const newCascade = messagePlan.cascade.map((item, index) => index === cascadeIndex - ? { ...item, defaultTemplateId: parsedForm.data.channelTemplate } + ? { + ...item, + defaultTemplateId: selectedTemplateId, + ...(selectedTemplate?.templateType === 'LETTER' && + selectedTemplate.supplierReferences + ? { supplierReferences: selectedTemplate.supplierReferences } + : {}), + } : item ); diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index cf458027a..0b592a070 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -269,6 +269,12 @@ }, "channelType": { "$ref": "#/components/schemas/ChannelType" + }, + "supplierReferences": { + "additionalProperties": { + "type": "string" + }, + "type": "object" } }, "required": [ @@ -343,6 +349,12 @@ "accessibleFormat": { "$ref": "#/components/schemas/LetterType" }, + "supplierReferences": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "templateId": { "format": "uuid", "nullable": true, @@ -361,6 +373,12 @@ "language": { "$ref": "#/components/schemas/Language" }, + "supplierReferences": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "templateId": { "format": "uuid", "nullable": true, @@ -643,6 +661,9 @@ "format": "date-time", "type": "string" }, + "defaultCascadeGroup": { + "type": "string" + }, "id": { "format": "uuid", "type": "string" @@ -663,6 +684,7 @@ "cascadeGroupOverrides", "cascade", "clientId", + "defaultCascadeGroup", "createdAt", "id", "name", @@ -766,6 +788,19 @@ ], "type": "object" }, + "TemplateStatus": { + "anyOf": [ + { + "$ref": "#/components/schemas/TemplateStatusActive" + }, + { + "enum": [ + "DELETED" + ], + "type": "string" + } + ] + }, "TemplateStatusActive": { "enum": [ "NOT_YET_SUBMITTED", @@ -780,19 +815,6 @@ ], "type": "string" }, - "TemplateStatus": { - "anyOf": [ - { - "$ref": "#/components/schemas/TemplateStatusActive" - }, - { - "enum": [ - "DELETED" - ], - "type": "string" - } - ] - }, "TemplateSuccess": { "properties": { "data": { diff --git a/lambdas/backend-api/README.md b/lambdas/backend-api/README.md index b0392eb48..a5c48da9c 100644 --- a/lambdas/backend-api/README.md +++ b/lambdas/backend-api/README.md @@ -180,7 +180,7 @@ curl -X POST --location "${APIG_STAGE}/v1/routing-configuration" \ "channelType": "primary", "defaultTemplateId": "email_id" }], - "cascadeGroupOverrides": [{ "name": "standard" }], + "cascadeGroupOverrides": [], "name": "RC name" }' ``` @@ -200,7 +200,7 @@ curl -X PUT --location "${APIG_STAGE}/v1/routing-configuration/${ROUTING_CONFIG_ "channelType": "primary", "defaultTemplateId": "email_id" }], - "cascadeGroupOverrides": [{ "name": "standard" }], + "cascadeGroupOverrides": [], "name": "New name" }' ``` diff --git a/lambdas/backend-api/src/__tests__/api/create-routing-config.test.ts b/lambdas/backend-api/src/__tests__/api/create-routing-config.test.ts index 278f01d7d..f0cbbcc70 100644 --- a/lambdas/backend-api/src/__tests__/api/create-routing-config.test.ts +++ b/lambdas/backend-api/src/__tests__/api/create-routing-config.test.ts @@ -134,7 +134,7 @@ describe('Create Routing Config Handler', () => { defaultTemplateId: 'apptemplate', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], name: 'app RC', campaignId: 'campaign', }; @@ -146,6 +146,7 @@ describe('Create Routing Config Handler', () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), clientId: 'nhs-notify-client-id', + defaultCascadeGroup: 'standard', }; mocks.routingConfigClient.createRoutingConfig.mockResolvedValueOnce({ diff --git a/lambdas/backend-api/src/__tests__/api/update-routing-config.test.ts b/lambdas/backend-api/src/__tests__/api/update-routing-config.test.ts index fcce4b71c..1b7b6831f 100644 --- a/lambdas/backend-api/src/__tests__/api/update-routing-config.test.ts +++ b/lambdas/backend-api/src/__tests__/api/update-routing-config.test.ts @@ -127,7 +127,7 @@ describe('Update Routing Config Handler', () => { channelType: 'primary', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], campaignId: 'campaign', name: 'new name', }; @@ -181,7 +181,7 @@ describe('Update Routing Config Handler', () => { channelType: 'primary', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], campaignId: 'campaign', name: 'new name', }; diff --git a/lambdas/backend-api/src/__tests__/app/routing-config-client.test.ts b/lambdas/backend-api/src/__tests__/app/routing-config-client.test.ts index 6624255d7..e93cef558 100644 --- a/lambdas/backend-api/src/__tests__/app/routing-config-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/routing-config-client.test.ts @@ -263,13 +263,14 @@ describe('RoutingConfigClient', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; const rc: RoutingConfig = { ...input, clientId: user.clientId, createdAt: date.toISOString(), + defaultCascadeGroup: 'standard', id: 'id', status: 'DRAFT', updatedAt: date.toISOString(), @@ -360,7 +361,7 @@ describe('RoutingConfigClient', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -402,7 +403,7 @@ describe('RoutingConfigClient', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -442,7 +443,7 @@ describe('RoutingConfigClient', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -477,7 +478,7 @@ describe('RoutingConfigClient', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ diff --git a/lambdas/backend-api/src/__tests__/fixtures/routing-config.ts b/lambdas/backend-api/src/__tests__/fixtures/routing-config.ts index 60fc2fd10..ef237d7a7 100644 --- a/lambdas/backend-api/src/__tests__/fixtures/routing-config.ts +++ b/lambdas/backend-api/src/__tests__/fixtures/routing-config.ts @@ -12,7 +12,8 @@ export const routingConfig: RoutingConfig = { defaultTemplateId: '90e46ece-4a3b-47bd-b781-f986b42a5a09', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', id: 'b9b6d56b-421e-462f-9ce5-3012e3fdb27f', status: 'DRAFT', name: 'Test config', diff --git a/lambdas/backend-api/src/__tests__/infra/routing-config-repository/repository.test.ts b/lambdas/backend-api/src/__tests__/infra/routing-config-repository/repository.test.ts index 08fab403e..45690562a 100644 --- a/lambdas/backend-api/src/__tests__/infra/routing-config-repository/repository.test.ts +++ b/lambdas/backend-api/src/__tests__/infra/routing-config-repository/repository.test.ts @@ -147,13 +147,14 @@ describe('RoutingConfigRepository', () => { defaultTemplateId: 'sms', }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; const rc: RoutingConfig = { ...input, clientId: user.clientId, createdAt: date.toISOString(), + defaultCascadeGroup: 'standard', id: generatedId, status: 'DRAFT', updatedAt: date.toISOString(), diff --git a/lambdas/backend-api/src/infra/routing-config-repository/repository.ts b/lambdas/backend-api/src/infra/routing-config-repository/repository.ts index 38c4f3f6d..e760b016a 100644 --- a/lambdas/backend-api/src/infra/routing-config-repository/repository.ts +++ b/lambdas/backend-api/src/infra/routing-config-repository/repository.ts @@ -46,6 +46,7 @@ export class RoutingConfigRepository { ...routingConfigInput, clientId: user.clientId, createdAt: date, + defaultCascadeGroup: 'standard', id: randomUUID(), status: 'DRAFT', updatedAt: date, diff --git a/lambdas/backend-client/src/__tests__/routing-config-api-client.test.ts b/lambdas/backend-client/src/__tests__/routing-config-api-client.test.ts index edab46a1f..997103811 100644 --- a/lambdas/backend-client/src/__tests__/routing-config-api-client.test.ts +++ b/lambdas/backend-client/src/__tests__/routing-config-api-client.test.ts @@ -223,6 +223,7 @@ describe('RoutingConfigurationApiClient', () => { campaignId: 'campaign-id', cascade: [], cascadeGroupOverrides: [], + defaultCascadeGroup: 'standard', clientId: 'client-id', createdAt: new Date().toISOString(), id: 'id', diff --git a/lambdas/backend-client/src/__tests__/schemas/__snapshots__/routing-config.test.ts.snap b/lambdas/backend-client/src/__tests__/schemas/__snapshots__/routing-config.test.ts.snap index fa243f1ec..b240732f0 100644 --- a/lambdas/backend-client/src/__tests__/schemas/__snapshots__/routing-config.test.ts.snap +++ b/lambdas/backend-client/src/__tests__/schemas/__snapshots__/routing-config.test.ts.snap @@ -73,6 +73,14 @@ exports[`RoutingConfig schema snapshot full error 1`] = ` ], "message": "Invalid input: expected array, received undefined" }, + { + "expected": "string", + "code": "invalid_type", + "path": [ + "defaultCascadeGroup" + ], + "message": "Invalid input: expected string, received undefined" + }, { "expected": "string", "code": "invalid_type", diff --git a/lambdas/backend-client/src/__tests__/schemas/routing-config.test.ts b/lambdas/backend-client/src/__tests__/schemas/routing-config.test.ts index 2a2e559a9..575ae856b 100644 --- a/lambdas/backend-client/src/__tests__/schemas/routing-config.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/routing-config.test.ts @@ -54,6 +54,7 @@ const baseCreated = { createdBy: 'user-1', updatedAt: '2025-09-18T15:26:04.338Z', updatedBy: 'user-1', + defaultCascadeGroup: 'standard', }; describe.each([ @@ -176,14 +177,6 @@ describe.each([ expect(res.success).toBe(false); }); - test('cascadeGroupOverrides must be nonempty', () => { - const res = $Schema.safeParse({ - ...baseInput, - cascadeGroupOverrides: [], - }); - expect(res.success).toBe(false); - }); - test('translations override languages must be nonempty', () => { const res = $Schema.safeParse({ ...baseInput, diff --git a/lambdas/backend-client/src/schemas/routing-config.ts b/lambdas/backend-client/src/schemas/routing-config.ts index e6a78ff54..42526aa73 100644 --- a/lambdas/backend-client/src/schemas/routing-config.ts +++ b/lambdas/backend-client/src/schemas/routing-config.ts @@ -66,6 +66,7 @@ const $ConditionalTemplateLanguage = schemaFor()( z.object({ language: $Language, templateId: z.string().nonempty().nullable(), + supplierReferences: z.record(z.string(), z.string()).optional(), }) ); @@ -74,6 +75,7 @@ const $ConditionalTemplateAccessible = z.object({ accessibleFormat: $LetterType, templateId: z.string().nonempty().nullable(), + supplierReferences: z.record(z.string(), z.string()).optional(), }) ); @@ -82,6 +84,7 @@ const $CascadeItemBase = schemaFor()( cascadeGroups: z.array($CascadeGroupName), channel: $Channel, channelType: $ChannelType, + supplierReferences: z.record(z.string(), z.string()).optional(), }) ); @@ -116,7 +119,7 @@ export const $CreateRoutingConfig = schemaFor()( z.object({ campaignId: z.string(), cascade: z.array($CascadeItem).nonempty(), - cascadeGroupOverrides: z.array($CascadeGroup).nonempty(), + cascadeGroupOverrides: z.array($CascadeGroup), name: z.string(), }) ); @@ -126,7 +129,7 @@ export const $UpdateRoutingConfig = schemaFor()( .object({ campaignId: z.string().optional(), cascade: z.array($CascadeItem).nonempty().optional(), - cascadeGroupOverrides: z.array($CascadeGroup).nonempty().optional(), + cascadeGroupOverrides: z.array($CascadeGroup).optional(), name: z.string().optional(), }) .strict() @@ -159,7 +162,8 @@ export const $RoutingConfig = schemaFor()( z.object({ campaignId: z.string(), cascade: z.array($CascadeItem).nonempty(), - cascadeGroupOverrides: z.array($CascadeGroup).nonempty(), + cascadeGroupOverrides: z.array($CascadeGroup), + defaultCascadeGroup: z.string(), name: z.string(), clientId: z.string(), id: z.uuidv4(), diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index 2f6cd1241..534e8af19 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -102,6 +102,7 @@ export const $LetterProperties = schemaFor()( files: $LetterFiles, personalisationParameters: z.array(z.string()).optional(), proofingEnabled: z.boolean().optional(), + supplierReferences: z.record(z.string(), z.string()).optional(), }) ); diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index e45bf3a3e..ec6779568 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -68,6 +68,9 @@ export type CascadeItemBase = { cascadeGroups: Array; channel: Channel; channelType: ChannelType; + supplierReferences?: { + [key: string]: string; + }; }; export type Channel = 'EMAIL' | 'LETTER' | 'NHSAPP' | 'SMS'; @@ -91,11 +94,17 @@ export type ClientFeatures = { export type ConditionalTemplateAccessible = { accessibleFormat: LetterType; + supplierReferences?: { + [key: string]: string; + }; templateId: string | null; }; export type ConditionalTemplateLanguage = { language: Language; + supplierReferences?: { + [key: string]: string; + }; templateId: string | null; }; @@ -195,6 +204,7 @@ export type RoutingConfig = { cascadeGroupOverrides: Array; clientId: string; createdAt: string; + defaultCascadeGroup: string; id: string; name: string; status: RoutingConfigStatus; @@ -223,6 +233,8 @@ export type SmsProperties = { export type TemplateDto = BaseCreatedTemplate & (SmsProperties | EmailProperties | NhsAppProperties | LetterProperties); +export type TemplateStatus = TemplateStatusActive | 'DELETED'; + export type TemplateStatusActive = | 'NOT_YET_SUBMITTED' | 'PENDING_PROOF_REQUEST' @@ -234,8 +246,6 @@ export type TemplateStatusActive = | 'WAITING_FOR_PROOF' | 'PROOF_AVAILABLE'; -export type TemplateStatus = TemplateStatusActive | 'DELETED'; - export type TemplateSuccess = { data: TemplateDto; statusCode: number; diff --git a/tests/test-team/helpers/factories/routing-config-factory.ts b/tests/test-team/helpers/factories/routing-config-factory.ts index 6141d3045..835160459 100644 --- a/tests/test-team/helpers/factories/routing-config-factory.ts +++ b/tests/test-team/helpers/factories/routing-config-factory.ts @@ -41,6 +41,7 @@ export const RoutingConfigFactory = { status: 'DRAFT', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), + defaultCascadeGroup: 'standard', ...routingConfig, ...apiPayload, }; diff --git a/tests/test-team/template-mgmt-api-tests/create-routing-configuration.api.spec.ts b/tests/test-team/template-mgmt-api-tests/create-routing-configuration.api.spec.ts index d39f1f93b..7b9184823 100644 --- a/tests/test-team/template-mgmt-api-tests/create-routing-configuration.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/create-routing-configuration.api.spec.ts @@ -60,6 +60,7 @@ test.describe('POST /v1/routing-configuration', () => { cascade: payload.cascade, cascadeGroupOverrides: payload.cascadeGroupOverrides, createdAt: expect.stringMatching(isoDateRegExp), + defaultCascadeGroup: 'standard', name: payload.name, id: expect.stringMatching(uuidRegExp), status: 'DRAFT', @@ -131,6 +132,7 @@ test.describe('POST /v1/routing-configuration', () => { cascade: payload.cascade, cascadeGroupOverrides: payload.cascadeGroupOverrides, createdAt: expect.stringMatching(isoDateRegExp), + defaultCascadeGroup: 'standard', name: payload.name, id: expect.stringMatching(uuidRegExp), status: 'DRAFT', diff --git a/tests/test-team/template-mgmt-api-tests/update-routing-config.api.spec.ts b/tests/test-team/template-mgmt-api-tests/update-routing-config.api.spec.ts index b1e3eb91a..7b0e53d5e 100644 --- a/tests/test-team/template-mgmt-api-tests/update-routing-config.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/update-routing-config.api.spec.ts @@ -389,7 +389,70 @@ test.describe('PUT /v1/routing-configuration/:routingConfigId', () => { defaultTemplateId: null, }, ], - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], + }; + + const start = new Date(); + + const updateResponse = await request.put( + `${process.env.API_BASE_URL}/v1/routing-configuration/${routingConfig.dbEntry.id}`, + { + headers: { + Authorization: await user1.getAccessToken(), + }, + data: update, + } + ); + + expect(updateResponse.status()).toBe(200); + + const updated = await updateResponse.json(); + + expect(updated).toEqual({ + statusCode: 200, + data: { + ...routingConfig.apiResponse, + ...update, + updatedAt: expect.stringMatching(isoDateRegExp), + }, + }); + + expect(updated.data.updatedAt).toBeDateRoughlyBetween([ + start, + new Date(), + ]); + expect(updated.data.createdAt).toEqual(routingConfig.dbEntry.createdAt); + }); + + test('cascade and cascadeGroupOverrides with supplierReferences - returns 200 and the updated routing config data', async ({ + request, + }) => { + const routingConfig = RoutingConfigFactory.create(user1, { + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: 'template-id', + supplierReferences: { + MBA: 'supplier-template-id', + }, + }, + ], + }); + + await storageHelper.seed([routingConfig.dbEntry]); + + const update = { + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + }, + ], + cascadeGroupOverrides: [], }; const start = new Date(); @@ -474,7 +537,7 @@ test.describe('PUT /v1/routing-configuration/:routingConfigId', () => { await storageHelper.seed([routingConfig.dbEntry]); const update = { - cascadeGroupOverrides: [{ name: 'standard' }], + cascadeGroupOverrides: [], }; const response = await request.put(