Skip to content

Commit 012007e

Browse files
committed
CCM-11494 Remove conditional templates action
1 parent 8edecff commit 012007e

File tree

4 files changed

+285
-20
lines changed

4 files changed

+285
-20
lines changed

frontend/src/__tests__/utils/routing-utils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ describe('removeTemplatesFromCascadeItem', () => {
277277

278278
const result = removeTemplatesFromCascadeItem(cascadeItem, ['template-1']);
279279

280+
expect(result.conditionalTemplates).toBeUndefined();
280281
expect(result.cascadeGroups).toEqual(['standard']);
281282
});
282283

frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.test.ts

Lines changed: 271 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { randomUUID } from 'node:crypto';
22
import { removeTemplateFromMessagePlan } from './actions';
33
import { getRoutingConfig, updateRoutingConfig } from '@utils/message-plans';
44
import {
5+
CascadeGroupAccessible,
56
CascadeGroupName,
7+
CascadeGroupTranslations,
68
Channel,
79
ChannelType,
10+
Language,
11+
LetterType,
812
RoutingConfigStatus,
913
} from 'nhs-notify-backend-client';
1014
import { redirect } from 'next/navigation';
@@ -18,6 +22,13 @@ const mockGetRoutingConfig = jest.mocked(getRoutingConfig);
1822
const mockUpdateRoutingConfig = jest.mocked(updateRoutingConfig);
1923

2024
const routingConfigId = randomUUID();
25+
const emailTemplateId = randomUUID();
26+
const smsTemplateId = randomUUID();
27+
const polishTemplateId = randomUUID();
28+
const frenchTemplateId = randomUUID();
29+
const accessibleFormatId = randomUUID();
30+
const largePrintId = randomUUID();
31+
2132
const baseConfig = {
2233
id: routingConfigId,
2334
campaignId: 'campaign1',
@@ -31,13 +42,13 @@ const baseConfig = {
3142
channel: 'EMAIL' as Channel,
3243
channelType: 'primary' as ChannelType,
3344
cascadeGroups: ['standard' as CascadeGroupName],
34-
defaultTemplateId: 'template-1',
45+
defaultTemplateId: emailTemplateId,
3546
},
3647
{
3748
channel: 'SMS' as Channel,
3849
channelType: 'primary' as ChannelType,
3950
cascadeGroups: ['standard' as CascadeGroupName],
40-
defaultTemplateId: 'template-2',
51+
defaultTemplateId: smsTemplateId,
4152
},
4253
],
4354
cascadeGroupOverrides: [],
@@ -48,12 +59,12 @@ describe('removeTemplateFromMessagePlan', () => {
4859
jest.clearAllMocks();
4960
});
5061

51-
it('removes the template from the correct channel and updates the routing configuration', async () => {
62+
it('removes the correct template from the cascade item and updates the routing configuration', async () => {
5263
mockGetRoutingConfig.mockResolvedValue(baseConfig);
5364

5465
const formData = new FormData();
5566
formData.set('routingConfigId', routingConfigId);
56-
formData.set('channel', 'EMAIL');
67+
formData.set('templateId', emailTemplateId);
5768

5869
await removeTemplateFromMessagePlan(formData);
5970

@@ -69,20 +80,272 @@ describe('removeTemplateFromMessagePlan', () => {
6980
}),
7081
expect.objectContaining({
7182
channel: 'SMS',
72-
defaultTemplateId: 'template-2',
83+
defaultTemplateId: smsTemplateId,
84+
}),
85+
],
86+
})
87+
);
88+
});
89+
90+
it('removes multiple templates at once', async () => {
91+
const configWithConditionalTemplates = {
92+
...baseConfig,
93+
cascade: [
94+
{
95+
...baseConfig.cascade[0],
96+
conditionalTemplates: [
97+
{
98+
accessibleFormat: 'x1' as LetterType,
99+
templateId: largePrintId,
100+
},
101+
{ language: 'pl' as Language, templateId: polishTemplateId },
102+
{ language: 'fr' as Language, templateId: frenchTemplateId },
103+
],
104+
},
105+
baseConfig.cascade[1],
106+
],
107+
};
108+
109+
mockGetRoutingConfig.mockResolvedValue(configWithConditionalTemplates);
110+
111+
const formData = new FormData();
112+
formData.set('routingConfigId', routingConfigId);
113+
formData.append('templateId', polishTemplateId);
114+
formData.append('templateId', frenchTemplateId);
115+
116+
await removeTemplateFromMessagePlan(formData);
117+
118+
expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(
119+
routingConfigId,
120+
expect.objectContaining({
121+
cascade: [
122+
expect.objectContaining({
123+
channel: 'EMAIL',
124+
defaultTemplateId: emailTemplateId,
125+
conditionalTemplates: [
126+
{
127+
accessibleFormat: 'x1',
128+
templateId: largePrintId,
129+
},
130+
],
131+
}),
132+
expect.objectContaining({
133+
channel: 'SMS',
134+
}),
135+
],
136+
})
137+
);
138+
});
139+
140+
it('removes conditional templates from cascade items', async () => {
141+
const configWithConditionalTemplate = {
142+
...baseConfig,
143+
cascade: [
144+
{
145+
...baseConfig.cascade[0],
146+
channel: 'LETTER' as Channel,
147+
conditionalTemplates: [
148+
{ accessibleFormat: 'x1' as LetterType, templateId: largePrintId },
149+
],
150+
},
151+
],
152+
};
153+
154+
mockGetRoutingConfig.mockResolvedValue(configWithConditionalTemplate);
155+
156+
const formData = new FormData();
157+
formData.set('routingConfigId', routingConfigId);
158+
formData.set('templateId', largePrintId);
159+
160+
await removeTemplateFromMessagePlan(formData);
161+
162+
expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(
163+
routingConfigId,
164+
expect.objectContaining({
165+
cascade: [
166+
expect.objectContaining({
167+
channel: 'LETTER',
168+
}),
169+
],
170+
})
171+
);
172+
173+
// Verify conditionalTemplates was removed
174+
const [[, updateConfig]] = mockUpdateRoutingConfig.mock.calls;
175+
expect(updateConfig.cascade?.[0].conditionalTemplates).toBeUndefined();
176+
});
177+
178+
it('updates cascadeGroupOverrides when templates are removed', async () => {
179+
const configWithOverrides = {
180+
...baseConfig,
181+
cascade: [
182+
{
183+
channel: 'LETTER' as Channel,
184+
channelType: 'primary' as ChannelType,
185+
cascadeGroups: [
186+
'standard' as CascadeGroupName,
187+
'accessible' as CascadeGroupName,
188+
'translations' as CascadeGroupName,
189+
],
190+
defaultTemplateId: emailTemplateId,
191+
conditionalTemplates: [
192+
{
193+
accessibleFormat: 'q4' as LetterType,
194+
templateId: accessibleFormatId,
195+
},
196+
{
197+
accessibleFormat: 'x1' as LetterType,
198+
templateId: largePrintId,
199+
},
200+
{ language: 'pl' as Language, templateId: polishTemplateId },
201+
{ language: 'fr' as Language, templateId: frenchTemplateId },
202+
],
203+
},
204+
],
205+
cascadeGroupOverrides: [
206+
{
207+
name: 'accessible' as CascadeGroupName,
208+
accessibleFormat: ['q4' as LetterType, 'x1' as LetterType],
209+
} as CascadeGroupAccessible,
210+
{
211+
name: 'translations' as CascadeGroupName,
212+
language: ['pl' as Language, 'fr' as Language],
213+
} as CascadeGroupTranslations,
214+
],
215+
};
216+
217+
mockGetRoutingConfig.mockResolvedValue(configWithOverrides);
218+
219+
const formData = new FormData();
220+
formData.set('routingConfigId', routingConfigId);
221+
formData.append('templateId', accessibleFormatId);
222+
formData.append('templateId', polishTemplateId);
223+
formData.append('templateId', frenchTemplateId);
224+
225+
await removeTemplateFromMessagePlan(formData);
226+
227+
expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(
228+
routingConfigId,
229+
expect.objectContaining({
230+
cascadeGroupOverrides: [
231+
{
232+
name: 'accessible',
233+
accessibleFormat: ['x1'],
234+
},
235+
],
236+
})
237+
);
238+
});
239+
240+
it('updates cascadeGroups on cascade items when templates are removed', async () => {
241+
const configWithConditionalTemplates = {
242+
...baseConfig,
243+
cascade: [
244+
{
245+
channel: 'LETTER' as Channel,
246+
channelType: 'primary' as ChannelType,
247+
cascadeGroups: [
248+
'standard' as CascadeGroupName,
249+
'accessible' as CascadeGroupName,
250+
'translations' as CascadeGroupName,
251+
],
252+
defaultTemplateId: emailTemplateId,
253+
conditionalTemplates: [
254+
{
255+
accessibleFormat: 'x1' as LetterType,
256+
templateId: largePrintId,
257+
},
258+
{ language: 'pl' as Language, templateId: polishTemplateId },
259+
{ language: 'fr' as Language, templateId: frenchTemplateId },
260+
],
261+
},
262+
],
263+
cascadeGroupOverrides: [],
264+
};
265+
266+
mockGetRoutingConfig.mockResolvedValue(configWithConditionalTemplates);
267+
268+
const formData = new FormData();
269+
formData.set('routingConfigId', routingConfigId);
270+
formData.append('templateId', polishTemplateId);
271+
formData.append('templateId', frenchTemplateId);
272+
273+
await removeTemplateFromMessagePlan(formData);
274+
275+
expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(
276+
routingConfigId,
277+
expect.objectContaining({
278+
cascade: [
279+
expect.objectContaining({
280+
channel: 'LETTER',
281+
cascadeGroups: ['standard', 'accessible'],
282+
conditionalTemplates: [
283+
{
284+
accessibleFormat: 'x1',
285+
templateId: largePrintId,
286+
},
287+
],
73288
}),
74289
],
75290
})
76291
);
77292
});
78293

294+
it('updates cascadeGroups to only standard when all conditional templates are removed', async () => {
295+
const configWithConditionalTemplates = {
296+
...baseConfig,
297+
cascade: [
298+
{
299+
channel: 'LETTER' as Channel,
300+
channelType: 'primary' as ChannelType,
301+
cascadeGroups: [
302+
'standard' as CascadeGroupName,
303+
'accessible' as CascadeGroupName,
304+
],
305+
defaultTemplateId: emailTemplateId,
306+
conditionalTemplates: [
307+
{
308+
accessibleFormat: 'x1' as LetterType,
309+
templateId: largePrintId,
310+
},
311+
],
312+
},
313+
],
314+
cascadeGroupOverrides: [],
315+
};
316+
317+
mockGetRoutingConfig.mockResolvedValue(configWithConditionalTemplates);
318+
319+
const formData = new FormData();
320+
formData.set('routingConfigId', routingConfigId);
321+
formData.set('templateId', largePrintId);
322+
323+
await removeTemplateFromMessagePlan(formData);
324+
325+
expect(mockUpdateRoutingConfig).toHaveBeenCalledWith(
326+
routingConfigId,
327+
expect.objectContaining({
328+
cascade: [
329+
expect.objectContaining({
330+
channel: 'LETTER',
331+
cascadeGroups: ['standard'],
332+
}),
333+
],
334+
})
335+
);
336+
337+
// Verify conditionalTemplates was removed
338+
const [[, updateConfig]] = mockUpdateRoutingConfig.mock.calls;
339+
expect(updateConfig.cascade?.[0].conditionalTemplates).toBeUndefined();
340+
});
341+
79342
it('refreshes the choose-templates page after successful removal', async () => {
80343
mockGetRoutingConfig.mockResolvedValue(baseConfig);
81344
mockUpdateRoutingConfig.mockResolvedValue(undefined);
82345

83346
const formData = new FormData();
84347
formData.set('routingConfigId', routingConfigId);
85-
formData.set('channel', 'EMAIL');
348+
formData.set('templateId', emailTemplateId);
86349

87350
await removeTemplateFromMessagePlan(formData);
88351

@@ -96,7 +359,7 @@ describe('removeTemplateFromMessagePlan', () => {
96359

97360
const formData = new FormData();
98361
formData.set('routingConfigId', routingConfigId);
99-
formData.set('channel', 'EMAIL');
362+
formData.set('templateId', emailTemplateId);
100363

101364
await expect(removeTemplateFromMessagePlan(formData)).rejects.toThrow(
102365
/not found/
@@ -114,7 +377,7 @@ describe('removeTemplateFromMessagePlan', () => {
114377
it('throws an error if form data is invalid', async () => {
115378
const formData = new FormData();
116379
formData.set('routingConfigId', 'invalid-id');
117-
formData.set('channel', 'test');
380+
formData.set('templateId', '');
118381

119382
await expect(removeTemplateFromMessagePlan(formData)).rejects.toThrow(
120383
/Invalid form data/

frontend/src/app/message-plans/choose-templates/[routingConfigId]/actions.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,18 @@ export async function removeTemplateFromMessagePlan(formData: FormData) {
1212
const parseResult = z
1313
.object({
1414
routingConfigId: z.uuidv4(),
15-
templateId: z.union([
16-
z.string().nonempty(),
17-
z.array(z.string().nonempty()),
18-
]),
15+
templateIds: z.array(z.uuidv4()).min(1),
1916
})
2017
.safeParse({
2118
routingConfigId: formData.get('routingConfigId'),
22-
templateId: formData.getAll('templateId'),
19+
templateIds: formData.getAll('templateId'),
2320
});
2421

2522
if (!parseResult.success) {
2623
throw new Error('Invalid form data');
2724
}
2825

29-
const { routingConfigId, templateId } = parseResult.data;
30-
const templateIds = Array.isArray(templateId) ? templateId : [templateId];
26+
const { routingConfigId, templateIds } = parseResult.data;
3127

3228
const routingConfig = await getRoutingConfig(routingConfigId);
3329

0 commit comments

Comments
 (0)