Skip to content

Commit 7ad5dba

Browse files
committed
CCM-11544 Optimistic locking for alternative letter types
1 parent 745bb31 commit 7ad5dba

File tree

8 files changed

+144
-21
lines changed

8 files changed

+144
-21
lines changed

frontend/src/__tests__/app/message-plans/choose-large-print-letter-template/page.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ describe('ChooseLargePrintLetterTemplate page', () => {
2727
params: Promise.resolve({
2828
routingConfigId: 'invalid-id',
2929
}),
30+
searchParams: Promise.resolve({
31+
lockNumber: '42',
32+
}),
3033
});
3134

3235
expect(getRoutingConfigMock).toHaveBeenCalledWith('invalid-id');
@@ -55,6 +58,9 @@ describe('ChooseLargePrintLetterTemplate page', () => {
5558
params: Promise.resolve({
5659
routingConfigId: ROUTING_CONFIG.id,
5760
}),
61+
searchParams: Promise.resolve({
62+
lockNumber: '42',
63+
}),
5864
});
5965

6066
expect(getRoutingConfigMock).toHaveBeenCalledWith(ROUTING_CONFIG.id);
@@ -73,6 +79,9 @@ describe('ChooseLargePrintLetterTemplate page', () => {
7379
params: Promise.resolve({
7480
routingConfigId: ROUTING_CONFIG.id,
7581
}),
82+
searchParams: Promise.resolve({
83+
lockNumber: '42',
84+
}),
7685
});
7786

7887
expect(getRoutingConfigMock).toHaveBeenCalledWith(ROUTING_CONFIG.id);
@@ -91,6 +100,9 @@ describe('ChooseLargePrintLetterTemplate page', () => {
91100
params: Promise.resolve({
92101
routingConfigId: ROUTING_CONFIG.id,
93102
}),
103+
searchParams: Promise.resolve({
104+
lockNumber: '42',
105+
}),
94106
});
95107

96108
const container = render(page);
@@ -107,4 +119,17 @@ describe('ChooseLargePrintLetterTemplate page', () => {
107119
});
108120
expect(container.asFragment()).toMatchSnapshot();
109121
});
122+
123+
it('redirects to choose templates page if the lockNumber is missing', async () => {
124+
await ChooseLargePrintLetterTemplate({
125+
params: Promise.resolve({
126+
routingConfigId: ROUTING_CONFIG.id,
127+
}),
128+
});
129+
130+
expect(redirectMock).toHaveBeenCalledWith(
131+
`/message-plans/choose-templates/${ROUTING_CONFIG.id}`,
132+
'replace'
133+
);
134+
});
110135
});

frontend/src/__tests__/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ describe('ChooseOtherLanguageLetterTemplate page', () => {
4444
params: Promise.resolve({
4545
routingConfigId: 'invalid-id',
4646
}),
47+
searchParams: Promise.resolve({
48+
lockNumber: '42',
49+
}),
4750
});
4851

4952
expect(getRoutingConfigMock).toHaveBeenCalledWith('invalid-id');
@@ -71,6 +74,9 @@ describe('ChooseOtherLanguageLetterTemplate page', () => {
7174
params: Promise.resolve({
7275
routingConfigId: ROUTING_CONFIG.id,
7376
}),
77+
searchParams: Promise.resolve({
78+
lockNumber: '42',
79+
}),
7480
});
7581

7682
expect(getRoutingConfigMock).toHaveBeenCalledWith(ROUTING_CONFIG.id);
@@ -92,6 +98,9 @@ describe('ChooseOtherLanguageLetterTemplate page', () => {
9298
params: Promise.resolve({
9399
routingConfigId: ROUTING_CONFIG.id,
94100
}),
101+
searchParams: Promise.resolve({
102+
lockNumber: '42',
103+
}),
95104
});
96105

97106
expect(getRoutingConfigMock).toHaveBeenCalledWith(ROUTING_CONFIG.id);
@@ -109,6 +118,9 @@ describe('ChooseOtherLanguageLetterTemplate page', () => {
109118
params: Promise.resolve({
110119
routingConfigId: ROUTING_CONFIG.id,
111120
}),
121+
searchParams: Promise.resolve({
122+
lockNumber: '42',
123+
}),
112124
});
113125

114126
const container = render(page);
@@ -121,4 +133,17 @@ describe('ChooseOtherLanguageLetterTemplate page', () => {
121133
});
122134
expect(container.asFragment()).toMatchSnapshot();
123135
});
136+
137+
it('redirects to choose templates page if the lockNumber is missing', async () => {
138+
await ChooseOtherLanguageLetterTemplate({
139+
params: Promise.resolve({
140+
routingConfigId: ROUTING_CONFIG.id,
141+
}),
142+
});
143+
144+
expect(redirectMock).toHaveBeenCalledWith(
145+
`/message-plans/choose-templates/${ROUTING_CONFIG.id}`,
146+
'replace'
147+
);
148+
});
124149
});

frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const renderComponent = (overrides = {}) => {
7676
pageHeading: 'Choose language letter templates',
7777
templateList: languageLetterTemplates,
7878
cascadeIndex: 3,
79+
lockNumber: 42,
7980
};
8081

8182
return render(

frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/server-action.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('chooseLanguageLetterTemplatesAction', () => {
6060
},
6161
getMockFormData({
6262
[`template_${FRENCH_LETTER.id}`]: `${FRENCH_LETTER.id}:fr`,
63+
lockNumber: '42',
6364
})
6465
);
6566

@@ -77,7 +78,8 @@ describe('chooseLanguageLetterTemplatesAction', () => {
7778
],
7879
}),
7980
],
80-
})
81+
}),
82+
42
8183
);
8284

8385
expect(mockRedirect).toHaveBeenCalledWith(
@@ -104,6 +106,7 @@ describe('chooseLanguageLetterTemplatesAction', () => {
104106
getMockFormData({
105107
[`template_${FRENCH_LETTER.id}`]: `${FRENCH_LETTER.id}:fr`,
106108
[`template_${POLISH_LETTER.id}`]: `${POLISH_LETTER.id}:pl`,
109+
lockNumber: '42',
107110
})
108111
);
109112

@@ -124,7 +127,8 @@ describe('chooseLanguageLetterTemplatesAction', () => {
124127
]),
125128
}),
126129
],
127-
})
130+
}),
131+
42
128132
);
129133
});
130134

@@ -157,6 +161,7 @@ describe('chooseLanguageLetterTemplatesAction', () => {
157161
},
158162
getMockFormData({
159163
[`template_${SPANISH_LETTER.id}`]: `${SPANISH_LETTER.id}:fr`,
164+
lockNumber: '42',
160165
})
161166
);
162167

@@ -173,7 +178,8 @@ describe('chooseLanguageLetterTemplatesAction', () => {
173178
],
174179
}),
175180
],
176-
})
181+
}),
182+
42
177183
);
178184
});
179185

@@ -206,6 +212,7 @@ describe('chooseLanguageLetterTemplatesAction', () => {
206212
},
207213
getMockFormData({
208214
[`template_${POLISH_LETTER.id}`]: `${POLISH_LETTER.id}:pl`,
215+
lockNumber: '42',
209216
})
210217
);
211218

@@ -226,7 +233,8 @@ describe('chooseLanguageLetterTemplatesAction', () => {
226233
]),
227234
}),
228235
],
229-
})
236+
}),
237+
42
230238
);
231239
});
232240

@@ -248,6 +256,7 @@ describe('chooseLanguageLetterTemplatesAction', () => {
248256
getMockFormData({
249257
[`template_${FRENCH_LETTER.id}`]: `${FRENCH_LETTER.id}:fr`,
250258
[`template_${FRENCH_LETTER_2.id}`]: `${FRENCH_LETTER_2.id}:fr`,
259+
lockNumber: '42',
251260
})
252261
);
253262

@@ -266,6 +275,7 @@ describe('$ChooseLanguageLetterTemplates Zod schema', () => {
266275
test('should pass validation when at least one template checkbox is selected', () => {
267276
const validData = {
268277
'template_abc-123': 'abc-123:fr',
278+
lockNumber: '42',
269279
};
270280

271281
const result = schema.safeParse(validData);
@@ -277,6 +287,7 @@ describe('$ChooseLanguageLetterTemplates Zod schema', () => {
277287
const validData = {
278288
'template_abc-123': 'abc-123:fr',
279289
'template_def-456': 'def-456:pl',
290+
lockNumber: '42',
280291
};
281292

282293
const result = schema.safeParse(validData);
@@ -285,7 +296,9 @@ describe('$ChooseLanguageLetterTemplates Zod schema', () => {
285296
});
286297

287298
test('should fail validation when no template checkboxes are selected', () => {
288-
const invalidData = {};
299+
const invalidData = {
300+
lockNumber: '42',
301+
};
289302

290303
const result = schema.safeParse(invalidData);
291304

@@ -296,6 +309,7 @@ describe('$ChooseLanguageLetterTemplates Zod schema', () => {
296309
test('should fail validation when only non-template fields are present', () => {
297310
const invalidData = {
298311
otherField: 'some-value',
312+
lockNumber: '42',
299313
};
300314

301315
const result = schema.safeParse(invalidData);

frontend/src/app/message-plans/choose-large-print-letter-template/[routingConfigId]/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getRoutingConfig } from '@utils/message-plans';
66
import { redirect, RedirectType } from 'next/navigation';
77
import { ChooseChannelTemplate } from '@forms/ChooseChannelTemplate';
88
import { getTemplates } from '@utils/form-actions';
9+
import { $LockNumber } from 'nhs-notify-backend-client';
910

1011
import content from '@content/content';
1112
const { pageTitle, pageHeading } = content.pages.chooseLargePrintLetterTemplate;
@@ -21,6 +22,17 @@ export default async function ChooseLargePrintLetterTemplate(
2122
) {
2223
const { routingConfigId } = await props.params;
2324

25+
const searchParams = await props.searchParams;
26+
27+
const lockNumberResult = $LockNumber.safeParse(searchParams?.lockNumber);
28+
29+
if (!lockNumberResult.success) {
30+
return redirect(
31+
`/message-plans/choose-templates/${routingConfigId}`,
32+
RedirectType.replace
33+
);
34+
}
35+
2436
const [messagePlan, availableTemplateList] = await Promise.all([
2537
getRoutingConfig(routingConfigId),
2638
getTemplates({
@@ -49,7 +61,7 @@ export default async function ChooseLargePrintLetterTemplate(
4961
templateList={availableTemplateList}
5062
cascadeIndex={cascadeIndex}
5163
accessibleFormat='x1'
52-
lockNumber={42}
64+
lockNumber={lockNumberResult.data}
5365
/>
5466
);
5567
}

frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getRoutingConfig } from '@utils/message-plans';
66
import { redirect, RedirectType } from 'next/navigation';
77
import { ChooseLanguageLetterTemplates } from '@forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates';
88
import { getForeignLanguageLetterTemplates } from '@utils/form-actions';
9+
import { $LockNumber } from 'nhs-notify-backend-client';
910

1011
import content from '@content/content';
1112
const { pageTitle, pageHeading } =
@@ -22,6 +23,17 @@ export default async function ChooseOtherLanguageLetterTemplate(
2223
) {
2324
const { routingConfigId } = await props.params;
2425

26+
const searchParams = await props.searchParams;
27+
28+
const lockNumberResult = $LockNumber.safeParse(searchParams?.lockNumber);
29+
30+
if (!lockNumberResult.success) {
31+
return redirect(
32+
`/message-plans/choose-templates/${routingConfigId}`,
33+
RedirectType.replace
34+
);
35+
}
36+
2537
const [messagePlan, foreignLanguageTemplates] = await Promise.all([
2638
getRoutingConfig(routingConfigId),
2739
getForeignLanguageLetterTemplates(),
@@ -45,6 +57,7 @@ export default async function ChooseOtherLanguageLetterTemplate(
4557
pageHeading={pageHeading}
4658
templateList={foreignLanguageTemplates}
4759
cascadeIndex={cascadeIndex}
60+
lockNumber={lockNumberResult.data}
4861
/>
4962
);
5063
}

frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ export type ChooseLanguageLetterTemplatesProps = {
3030
pageHeading: string;
3131
templateList: LetterTemplate[];
3232
cascadeIndex: number;
33+
lockNumber: number;
3334
};
3435

3536
export function ChooseLanguageLetterTemplates(
3637
props: ChooseLanguageLetterTemplatesProps
3738
) {
38-
const { messagePlan, pageHeading, templateList, cascadeIndex } = props;
39+
const { messagePlan, pageHeading, templateList, cascadeIndex, lockNumber } =
40+
props;
3941

4042
const [state, action] = useActionState(chooseLanguageLetterTemplatesAction, {
4143
...props,
@@ -80,6 +82,12 @@ export function ChooseLanguageLetterTemplates(
8082
formId={'choose-language-letter-templates'}
8183
formAttributes={{ onSubmit: formValidate }}
8284
>
85+
<input
86+
type='hidden'
87+
name='lockNumber'
88+
value={lockNumber}
89+
readOnly
90+
/>
8391
{selectedLanguageTemplateIds.length > 0 && (
8492
<Details data-testid='previous-selection-details'>
8593
<Details.Summary>

0 commit comments

Comments
 (0)