From add6b3492d6a674465dc7e996b558e06b26ac929 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Thu, 29 Jan 2026 22:18:09 +0000 Subject: [PATCH 01/74] api spec --- .../components/forms/SubmitTemplate/x.ts | 15 ++ .../modules/backend-api/spec.tmpl.json | 151 ++++++++++++------ .../src/types/generated/index.ts | 5 +- .../src/types/generated/types.gen.ts | 29 +++- 4 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 frontend/src/__tests__/components/forms/SubmitTemplate/x.ts diff --git a/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts b/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts new file mode 100644 index 000000000..22ad9d3d2 --- /dev/null +++ b/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts @@ -0,0 +1,15 @@ +const template: LetterTemplate = { + letterVersion: 'AUTHORING', + campaignId: 'campaignId', + createdAt: '2025-01-13T10:19:25.579Z', + id: 'templateId', + letterVariantId: 'letterVariantId', + sidesCount: 1, + language: 'en', + letterType: 'x0', + name: 'template-name', + templateStatus: 'NOT_YET_SUBMITTED', + templateType: 'LETTER', + updatedAt: '2025-01-13T10:19:25.579Z', + lockNumber: 1, +}; diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index c74e202ba..ada900ddc 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -1,6 +1,35 @@ { "components": { "schemas": { + "AuthoringLetterProperties": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseLetterTemplateProperties" + }, + { + "properties": { + "letterVariantId": { + "type": "string" + }, + "letterVersion": { + "enum": [ + "AUTHORING" + ], + "type": "string" + }, + "sidesCount": { + "type": "integer" + } + }, + "required": [ + "letterVariantId", + "letterVersion", + "sidesCount" + ], + "type": "object" + } + ] + }, "BaseCreatedTemplate": { "allOf": [ { @@ -52,9 +81,6 @@ }, "BaseLetterTemplateProperties": { "properties": { - "files": { - "$ref": "#/components/schemas/LetterFiles" - }, "language": { "$ref": "#/components/schemas/Language" }, @@ -417,6 +443,23 @@ ], "type": "object" }, + "CreatePdfProofingLetterProperties": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseLetterTemplateProperties" + }, + { + "properties": { + "campaignId": { + "type": "string" + } + }, + "required": [ + "campaignId" + ] + } + ] + }, "CreateRoutingConfig": { "properties": { "campaignId": { @@ -463,7 +506,7 @@ "$ref": "#/components/schemas/NhsAppProperties" }, { - "$ref": "#/components/schemas/UploadLetterProperties" + "$ref": "#/components/schemas/CreatePdfProofingLetterProperties" } ] } @@ -563,35 +606,19 @@ "type": "object" }, "LetterProperties": { - "allOf": [ + "discriminator": { + "mapping": { + "AUTHORING": "#/components/schemas/AuthoringLetterProperties", + "PDF_PROOFING": "#/components/schemas/PdfProofingLetterProperties" + }, + "propertyName": "letterVersion" + }, + "oneOf": [ { - "$ref": "#/components/schemas/BaseLetterTemplateProperties" + "$ref": "#/components/schemas/AuthoringLetterProperties" }, { - "properties": { - "files": { - "$ref": "#/components/schemas/LetterFiles" - }, - "personalisationParameters": { - "items": { - "type": "string" - }, - "type": "array" - }, - "proofingEnabled": { - "type": "boolean" - }, - "supplierReferences": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "required": [ - "files" - ], - "type": "object" + "$ref": "#/components/schemas/PdfProofingLetterProperties" } ] }, @@ -603,6 +630,13 @@ ], "type": "string" }, + "LetterVersion": { + "enum": [ + "AUTHORING", + "PDF_PROOFING" + ], + "type": "string" + }, "NhsAppProperties": { "properties": { "message": { @@ -621,6 +655,46 @@ ], "type": "object" }, + "PdfProofingLetterProperties": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseLetterTemplateProperties" + }, + { + "properties": { + "files": { + "$ref": "#/components/schemas/LetterFiles" + }, + "letterVersion": { + "enum": [ + "PDF_PROOFING" + ], + "type": "string" + }, + "personalisationParameters": { + "items": { + "type": "string" + }, + "type": "array" + }, + "proofingEnabled": { + "type": "boolean" + }, + "supplierReferences": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "required": [ + "files", + "letterVersion" + ], + "type": "object" + } + ] + }, "ProofFileDetails": { "properties": { "fileName": { @@ -948,23 +1022,6 @@ }, "type": "object" }, - "UploadLetterProperties": { - "allOf": [ - { - "$ref": "#/components/schemas/BaseLetterTemplateProperties" - }, - { - "properties": { - "campaignId": { - "type": "string" - } - }, - "required": [ - "campaignId" - ] - } - ] - }, "UploadLetterTemplate": { "properties": { "letterPdf": { diff --git a/lambdas/backend-client/src/types/generated/index.ts b/lambdas/backend-client/src/types/generated/index.ts index 3fe3014ad..6ae023ac6 100644 --- a/lambdas/backend-client/src/types/generated/index.ts +++ b/lambdas/backend-client/src/types/generated/index.ts @@ -1,6 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type { + AuthoringLetterProperties, BaseCreatedTemplate, BaseLetterTemplateProperties, BaseTemplate, @@ -21,6 +22,7 @@ export type { ConditionalTemplateAccessible, ConditionalTemplateLanguage, CountSuccess, + CreatePdfProofingLetterProperties, CreateRoutingConfig, CreateUpdateTemplate, DeleteV1RoutingConfigurationByRoutingConfigIdData, @@ -74,6 +76,7 @@ export type { LetterFiles, LetterProperties, LetterType, + LetterVersion, NhsAppProperties, PatchV1RoutingConfigurationByRoutingConfigIdData, PatchV1RoutingConfigurationByRoutingConfigIdError, @@ -90,6 +93,7 @@ export type { PatchV1TemplateByTemplateIdSubmitErrors, PatchV1TemplateByTemplateIdSubmitResponse, PatchV1TemplateByTemplateIdSubmitResponses, + PdfProofingLetterProperties, PostV1LetterTemplateData, PostV1LetterTemplateError, PostV1LetterTemplateErrors, @@ -131,7 +135,6 @@ export type { TemplateSuccessList, TemplateType, UpdateRoutingConfig, - UploadLetterProperties, UploadLetterTemplate, VersionedFileDetails, VirusScanStatus, diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index 1fe54e66f..906f037c3 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -4,6 +4,12 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; +export type AuthoringLetterProperties = BaseLetterTemplateProperties & { + letterVariantId: string; + letterVersion: 'AUTHORING'; + sidesCount: number; +}; + export type BaseCreatedTemplate = BaseTemplate & { campaignId?: string; clientId?: string; @@ -17,7 +23,6 @@ export type BaseCreatedTemplate = BaseTemplate & { }; export type BaseLetterTemplateProperties = { - files?: LetterFiles; language: Language; letterType: LetterType; templateType: 'LETTER'; @@ -128,7 +133,12 @@ export type CreateRoutingConfig = { }; export type CreateUpdateTemplate = BaseTemplate & - (SmsProperties | EmailProperties | NhsAppProperties | UploadLetterProperties); + ( + | SmsProperties + | EmailProperties + | NhsAppProperties + | CreatePdfProofingLetterProperties + ); export type EmailProperties = { message: string; @@ -173,6 +183,8 @@ export type Language = | 'ur' | 'zh'; +export type LetterVersion = 'AUTHORING' | 'PDF_PROOFING'; + export type LetterFiles = { pdfTemplate: VersionedFileDetails; proofs?: { @@ -181,8 +193,17 @@ export type LetterFiles = { testDataCsv?: VersionedFileDetails; }; -export type LetterProperties = BaseLetterTemplateProperties & { +export type LetterProperties = + | ({ + letterVersion: 'AUTHORING'; + } & AuthoringLetterProperties) + | ({ + letterVersion: 'PDF_PROOFING'; + } & PdfProofingLetterProperties); + +export type PdfProofingLetterProperties = BaseLetterTemplateProperties & { files: LetterFiles; + letterVersion: 'PDF_PROOFING'; personalisationParameters?: Array; proofingEnabled?: boolean; supplierReferences?: { @@ -282,7 +303,7 @@ export type UpdateRoutingConfig = unknown & { name?: string; }; -export type UploadLetterProperties = BaseLetterTemplateProperties & { +export type CreatePdfProofingLetterProperties = BaseLetterTemplateProperties & { campaignId: string; }; From 9d77bb1646cb21cac3e222e528f6da6347175d15 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Thu, 29 Jan 2026 23:41:24 +0000 Subject: [PATCH 02/74] validate new letter version --- .../__tests__/app/copy-template/page.test.tsx | 1 + .../app/preview-letter-template/page.test.tsx | 8 +- .../page.test.tsx | 1 + .../request-proof-of-template/page.test.tsx | 2 + .../app/submit-letter-template/page.test.tsx | 4 +- .../LetterTemplateForm/server-action.test.ts | 2 + .../forms/RequestProof/server-action.test.ts | 1 + .../SubmitTemplate/server-action.test.ts | 1 + .../components/forms/SubmitTemplate/x.ts | 15 -- .../molecules/MessageTemplates.test.tsx | 3 + .../PreviewSubmittedTemplate.test.tsx | 1 + .../molecules/PreviewTemplateDetails.test.tsx | 5 + .../organisms/PreviewLetterTemplate.test.tsx | 11 ++ frontend/src/__tests__/helpers/helpers.ts | 8 +- .../src/__tests__/utils/form-actions.test.ts | 3 + .../ChooseChannelTemplate/server-action.ts | 4 +- .../server-action.ts | 18 +- .../PreviewTemplateDetailsLetter.tsx | 4 +- frontend/src/utils/routing-utils.ts | 25 ++- .../modules/backend-api/spec.tmpl.json | 7 - .../src/__tests__/api/list.test.ts | 2 + .../src/__tests__/api/proof.test.ts | 1 + .../src/__tests__/api/upload-letter.test.ts | 2 + .../src/__tests__/app/template-client.test.ts | 10 ++ .../src/__tests__/fixtures/template.ts | 11 +- .../infra/template-repository/repository.ts | 1 + .../src/__tests__/schemas/template.test.ts | 160 +++++++++++++++++- .../backend-client/src/schemas/template.ts | 93 +++++++--- .../src/types/generated/types.gen.ts | 34 ++-- tests/accessibility/pa11y-setup.ts | 1 + utils/utils/jest.config.ts | 1 + .../utils/src/__tests__/email-client.test.ts | 6 +- utils/utils/src/__tests__/types.test.ts | 60 +++++++ utils/utils/src/email-client.ts | 10 +- utils/utils/src/types.ts | 31 +++- utils/utils/src/zod-validators.ts | 8 +- 36 files changed, 453 insertions(+), 102 deletions(-) delete mode 100644 frontend/src/__tests__/components/forms/SubmitTemplate/x.ts create mode 100644 utils/utils/src/__tests__/types.test.ts diff --git a/frontend/src/__tests__/app/copy-template/page.test.tsx b/frontend/src/__tests__/app/copy-template/page.test.tsx index da4e17e1a..a830c104a 100644 --- a/frontend/src/__tests__/app/copy-template/page.test.tsx +++ b/frontend/src/__tests__/app/copy-template/page.test.tsx @@ -40,6 +40,7 @@ describe('CopyTemplatePage', () => { lockNumber: 1, letterType: 'q4', language: 'fr', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx index 5b5df037c..e7b0b74c2 100644 --- a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx @@ -5,7 +5,10 @@ import PreviewLetterTemplatePage, { generateMetadata, } from '@app/preview-letter-template/[templateId]/page'; import { PreviewLetterTemplate } from '@organisms/PreviewLetterTemplate/PreviewLetterTemplate'; -import { type LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { + type PdfProofingLetterTemplate, + type LetterTemplate, +} from 'nhs-notify-web-template-management-utils'; import { redirect } from 'next/navigation'; import { getTemplate } from '@utils/form-actions'; import { Language, LetterType, TemplateDto } from 'nhs-notify-backend-client'; @@ -41,6 +44,7 @@ const templateDTO = { id: 'template-id', language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', name: 'template-name', templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', @@ -122,7 +126,7 @@ describe('PreviewLetterTemplatePage', () => { }, { description: 'a letter where files is the wrong data type', - files: [] as unknown as LetterTemplate['files'], + files: [] as unknown as PdfProofingLetterTemplate['files'], }, ])( 'should redirect to invalid-template when template is $description', diff --git a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx index 8816018fc..8b8b59ddd 100644 --- a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx @@ -42,6 +42,7 @@ describe('PreviewSubmittedLetterTemplatePage', () => { id: 'template-id', language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', name: 'template-name', templateStatus: 'SUBMITTED', templateType: 'LETTER', diff --git a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx index 7fcffbab9..6b732c9d1 100644 --- a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx +++ b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx @@ -41,6 +41,7 @@ describe('RequestProofPage', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', proofingEnabled: true, files: { @@ -144,6 +145,7 @@ describe('RequestProofPage', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'ar', proofingEnabled: false, files: { diff --git a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx index a0bad94b3..c3b93cfcf 100644 --- a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx @@ -14,7 +14,7 @@ import { NHS_APP_TEMPLATE, SMS_TEMPLATE, } from '@testhelpers/helpers'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfProofingLetterTemplate } from 'nhs-notify-web-template-management-utils'; import content from '@content/content'; import { serverIsFeatureEnabled } from '@utils/server-features'; @@ -74,7 +74,7 @@ describe('SubmitLetterTemplatePage', () => { SMS_TEMPLATE, { ...LETTER_TEMPLATE, - files: undefined as unknown as LetterTemplate['files'], + files: undefined as unknown as PdfProofingLetterTemplate['files'], } as TemplateDto, { ...LETTER_TEMPLATE, diff --git a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts index 3325c0ad4..25b3e2773 100644 --- a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts @@ -51,6 +51,7 @@ describe('UploadLetterTemplate server actions', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x1', + letterVersion: 'PDF_PROOFING', language: 'ar', files: { pdfTemplate: { @@ -113,6 +114,7 @@ describe('UploadLetterTemplate server actions', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x1', + letterVersion: 'PDF_PROOFING', language: 'ar', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts b/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts index 97c296688..d97077ddd 100644 --- a/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts @@ -25,6 +25,7 @@ const mockLetterTemplate = (id: string) => createdAt: '2025-01-13T10:19:25.579Z', updatedAt: '2025-01-13T10:19:25.579Z', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts index 5f6567376..6e313750c 100644 --- a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts @@ -151,6 +151,7 @@ describe('submitTemplate', () => { id: templateId, language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', name: 'template-name', templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', diff --git a/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts b/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts deleted file mode 100644 index 22ad9d3d2..000000000 --- a/frontend/src/__tests__/components/forms/SubmitTemplate/x.ts +++ /dev/null @@ -1,15 +0,0 @@ -const template: LetterTemplate = { - letterVersion: 'AUTHORING', - campaignId: 'campaignId', - createdAt: '2025-01-13T10:19:25.579Z', - id: 'templateId', - letterVariantId: 'letterVariantId', - sidesCount: 1, - language: 'en', - letterType: 'x0', - name: 'template-name', - templateStatus: 'NOT_YET_SUBMITTED', - templateType: 'LETTER', - updatedAt: '2025-01-13T10:19:25.579Z', - lockNumber: 1, -}; diff --git a/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx b/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx index 1053a9388..ca38e62d1 100644 --- a/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx +++ b/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx @@ -44,6 +44,7 @@ const messageTemplatesProps: { name: 'Template 3', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, @@ -62,6 +63,7 @@ const messageTemplatesProps: { name: 'Template 4', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'fr', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, @@ -118,6 +120,7 @@ describe('MessageTemplates component', () => { name: 'Template 1', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, diff --git a/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx b/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx index 5ac02b0d5..6fb531417 100644 --- a/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx @@ -86,6 +86,7 @@ describe('PreviewSubmittedTemplate component', () => { templateStatus: 'SUBMITTED', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index 380cc4319..b8aa0ec0b 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -78,6 +78,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PENDING_VALIDATION', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'fr', files: { pdfTemplate: { @@ -110,6 +111,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PENDING_VALIDATION', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'fr', files: { pdfTemplate: { @@ -138,6 +140,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PROOF_AVAILABLE', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -187,6 +190,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PROOF_AVAILABLE', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -214,6 +218,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx index 9454dc822..83b7cf0b4 100644 --- a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx +++ b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx @@ -20,6 +20,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'VIRUS_SCAN_FAILED', language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', @@ -48,6 +49,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PENDING_PROOF_REQUEST', letterType: 'q4', + letterVersion: 'PDF_PROOFING', language: 'ar', files: { pdfTemplate: { @@ -76,6 +78,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'WAITING_FOR_PROOF', letterType: 'q4', + letterVersion: 'PDF_PROOFING', language: 'ar', files: { pdfTemplate: { @@ -105,6 +108,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_AVAILABLE', letterType: 'x1', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -142,6 +146,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_AVAILABLE', letterType: 'x1', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -180,6 +185,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_APPROVED', letterType: 'x1', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -215,6 +221,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', files: { pdfTemplate: { @@ -251,6 +258,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'VALIDATION_FAILED', language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', @@ -284,6 +292,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'PENDING_UPLOAD', language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', @@ -312,6 +321,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'PENDING_VALIDATION', language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', @@ -339,6 +349,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'ar', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/helpers/helpers.ts b/frontend/src/__tests__/helpers/helpers.ts index 7c643a019..b7d7799df 100644 --- a/frontend/src/__tests__/helpers/helpers.ts +++ b/frontend/src/__tests__/helpers/helpers.ts @@ -1,6 +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'; +import { PdfProofingLetterTemplate } from 'nhs-notify-web-template-management-utils'; function* iteratorFromList(list: T[]): IterableIterator { for (const item of list) { @@ -52,12 +52,13 @@ export const SMS_TEMPLATE: TemplateDto = { lockNumber: 1, } as const; -export const LETTER_TEMPLATE: LetterTemplate = { +export const LETTER_TEMPLATE: PdfProofingLetterTemplate = { id: 'letter-template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', language: 'en', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'template.pdf', @@ -71,12 +72,13 @@ export const LETTER_TEMPLATE: LetterTemplate = { lockNumber: 1, } as const; -export const LARGE_PRINT_LETTER_TEMPLATE: LetterTemplate = { +export const LARGE_PRINT_LETTER_TEMPLATE: PdfProofingLetterTemplate = { id: 'large-print-letter-template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x1', language: 'en', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'large-print-template.pdf', diff --git a/frontend/src/__tests__/utils/form-actions.test.ts b/frontend/src/__tests__/utils/form-actions.test.ts index c3cac9bb0..dc5b44a3e 100644 --- a/frontend/src/__tests__/utils/form-actions.test.ts +++ b/frontend/src/__tests__/utils/form-actions.test.ts @@ -128,6 +128,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'template.pdf', @@ -188,6 +189,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'template.pdf', @@ -774,6 +776,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'template.pdf', diff --git a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts index 498134f99..2a4bd371d 100644 --- a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts +++ b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts @@ -5,6 +5,7 @@ import { updateRoutingConfig } from '@utils/message-plans'; import { ChooseChannelTemplateProps } from './choose-channel-template.types'; import { isLetterTemplate, + isPdfProofingLetter, addAccessibleFormatLetterTemplateToCascade, addDefaultTemplateToCascade, } from '@utils/routing-utils'; @@ -55,7 +56,8 @@ export async function chooseChannelTemplateAction( const updatedCascade = isAccessibleFormatTemplate && selectedTemplate && - isLetterTemplate(selectedTemplate) + isLetterTemplate(selectedTemplate) && + isPdfProofingLetter(selectedTemplate) ? addAccessibleFormatLetterTemplateToCascade( messagePlan.cascade, cascadeIndex, diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts index ef44d88c6..d4eb06089 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts @@ -1,8 +1,14 @@ import { redirect, RedirectType } from 'next/navigation'; -import { FormState } from 'nhs-notify-web-template-management-utils'; +import { + FormState, + PdfProofingLetterTemplate, +} from 'nhs-notify-web-template-management-utils'; import { z } from 'zod'; import { updateRoutingConfig } from '@utils/message-plans'; -import { replaceLanguageTemplatesInCascadeItem } from '@utils/routing-utils'; +import { + replaceLanguageTemplatesInCascadeItem, + isPdfProofingLetter, +} from '@utils/routing-utils'; import { Language, $LockNumber } from 'nhs-notify-backend-client'; import { ChooseLanguageLetterTemplatesProps } from './ChooseLanguageLetterTemplates'; import baseContent from '@content/content'; @@ -85,9 +91,13 @@ export async function chooseLanguageLetterTemplatesAction( } const selectedTemplateIdsSet = new Set(selectedTemplateIds); - const templateMap = new Map( + const templateMap = new Map( templateList - .filter((template) => selectedTemplateIdsSet.has(template.id)) + .filter( + (template): template is PdfProofingLetterTemplate => + selectedTemplateIdsSet.has(template.id) && + isPdfProofingLetter(template) + ) .map((template) => [template.id, template]) ); diff --git a/frontend/src/components/molecules/PreviewTemplateDetails/PreviewTemplateDetailsLetter.tsx b/frontend/src/components/molecules/PreviewTemplateDetails/PreviewTemplateDetailsLetter.tsx index 3755fdee0..55d96af04 100644 --- a/frontend/src/components/molecules/PreviewTemplateDetails/PreviewTemplateDetailsLetter.tsx +++ b/frontend/src/components/molecules/PreviewTemplateDetails/PreviewTemplateDetailsLetter.tsx @@ -2,6 +2,7 @@ import { Container, SummaryList } from 'nhsuk-react-components'; import { + assertPdfProofingLetter, letterTypeDisplayMappings, type LetterTemplate, } from 'nhs-notify-web-template-management-utils'; @@ -21,13 +22,14 @@ import { useFeatureFlags } from '@providers/client-config-provider'; const { rowHeadings } = content.components.previewTemplateDetails; export default function PreviewTemplateDetailsLetter({ - template, + template: letterTemplate, hideStatus, }: { template: LetterTemplate; hideStatus?: boolean; }) { const features = useFeatureFlags(); + const template = assertPdfProofingLetter(letterTemplate); const proofFilenames = Object.values(template.files.proofs ?? {}) .filter(({ virusScanStatus }) => virusScanStatus === 'PASSED') diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts index f2e92717c..bb7c7e43a 100644 --- a/frontend/src/utils/routing-utils.ts +++ b/frontend/src/utils/routing-utils.ts @@ -7,7 +7,10 @@ import { RoutingConfig, TemplateDto, } from 'nhs-notify-backend-client'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { + LetterTemplate, + PdfProofingLetterTemplate, +} from 'nhs-notify-web-template-management-utils'; export type ConditionalTemplate = | ConditionalTemplateAccessible @@ -24,6 +27,15 @@ export function isLetterTemplate( return template.templateType === 'LETTER'; } +/** + * Type guard to check if a letter template is a PDF proofing letter + */ +export function isPdfProofingLetter( + template: LetterTemplate +): template is PdfProofingLetterTemplate { + return template.letterVersion === 'PDF_PROOFING'; +} + /** * Gets the conditional templates for a cascade item, from the provided templates object */ @@ -193,6 +205,7 @@ export function addDefaultTemplateToCascade( defaultTemplateId: selectedTemplateId, ...(selectedTemplate && isLetterTemplate(selectedTemplate) && + isPdfProofingLetter(selectedTemplate) && selectedTemplate.supplierReferences && { supplierReferences: selectedTemplate.supplierReferences, }), @@ -206,7 +219,7 @@ export function addDefaultTemplateToCascade( */ export function addAccessibleFormatLetterTemplateToCascadeItem( cascadeItem: CascadeItem, - selectedTemplate: LetterTemplate + selectedTemplate: PdfProofingLetterTemplate ): CascadeItem { const newConditionalTemplate: ConditionalTemplateAccessible = { accessibleFormat: selectedTemplate.letterType, @@ -240,7 +253,7 @@ export function addAccessibleFormatLetterTemplateToCascadeItem( export function addAccessibleFormatLetterTemplateToCascade( cascade: CascadeItem[], cascadeIndex: number, - selectedTemplate: LetterTemplate + selectedTemplate: PdfProofingLetterTemplate ): CascadeItem[] { const updatedCascade = [...cascade]; updatedCascade[cascadeIndex] = addAccessibleFormatLetterTemplateToCascadeItem( @@ -255,7 +268,7 @@ export function addAccessibleFormatLetterTemplateToCascade( */ export function addLanguageLetterTemplatesToCascadeItem( cascadeItem: CascadeItem, - selectedTemplates: LetterTemplate[] + selectedTemplates: PdfProofingLetterTemplate[] ): CascadeItem { if (selectedTemplates.length === 0) { return cascadeItem; @@ -290,7 +303,7 @@ export function addLanguageLetterTemplatesToCascadeItem( export function addLanguageLetterTemplatesToCascade( cascade: CascadeItem[], cascadeIndex: number, - selectedTemplates: LetterTemplate[] + selectedTemplates: PdfProofingLetterTemplate[] ): CascadeItem[] { const updatedCascade = [...cascade]; updatedCascade[cascadeIndex] = addLanguageLetterTemplatesToCascadeItem( @@ -330,7 +343,7 @@ export function removeLanguageTemplatesFromCascadeItem( */ export function replaceLanguageTemplatesInCascadeItem( cascadeItem: CascadeItem, - selectedTemplates: LetterTemplate[] + selectedTemplates: PdfProofingLetterTemplate[] ): CascadeItem { const cascadeItemWithoutLanguages = removeLanguageTemplatesFromCascadeItem(cascadeItem); diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index ada900ddc..a1e8df838 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -606,13 +606,6 @@ "type": "object" }, "LetterProperties": { - "discriminator": { - "mapping": { - "AUTHORING": "#/components/schemas/AuthoringLetterProperties", - "PDF_PROOFING": "#/components/schemas/PdfProofingLetterProperties" - }, - "propertyName": "letterVersion" - }, "oneOf": [ { "$ref": "#/components/schemas/AuthoringLetterProperties" diff --git a/lambdas/backend-api/src/__tests__/api/list.test.ts b/lambdas/backend-api/src/__tests__/api/list.test.ts index 6ab303630..d3261e68e 100644 --- a/lambdas/backend-api/src/__tests__/api/list.test.ts +++ b/lambdas/backend-api/src/__tests__/api/list.test.ts @@ -244,6 +244,7 @@ describe('Template API - List', () => { lockNumber: 1, language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', @@ -305,6 +306,7 @@ describe('Template API - List', () => { lockNumber: 1, language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/lambdas/backend-api/src/__tests__/api/proof.test.ts b/lambdas/backend-api/src/__tests__/api/proof.test.ts index 3026a5417..7c047a2b5 100644 --- a/lambdas/backend-api/src/__tests__/api/proof.test.ts +++ b/lambdas/backend-api/src/__tests__/api/proof.test.ts @@ -166,6 +166,7 @@ describe('Template API - request proof', () => { lockNumber: 1, letterType: 'q4', language: 'fr', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts index 562cdf0b2..6142de4bf 100644 --- a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts +++ b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts @@ -87,6 +87,7 @@ describe('upload-letter', () => { updatedAt: now, lockNumber: 1, templateStatus: 'PENDING_VALIDATION', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: pdfFilename, @@ -162,6 +163,7 @@ describe('upload-letter', () => { lockNumber: 1, clientId, templateStatus: 'PENDING_VALIDATION', + letterVersion: 'PDF_PROOFING', files: { pdfTemplate: { fileName: pdfFilename, diff --git a/lambdas/backend-api/src/__tests__/app/template-client.test.ts b/lambdas/backend-api/src/__tests__/app/template-client.test.ts index de222738c..54f8a0cdc 100644 --- a/lambdas/backend-api/src/__tests__/app/template-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/template-client.test.ts @@ -387,6 +387,7 @@ describe('templateClient', () => { templateStatus: 'PENDING_UPLOAD', owner: `CLIENT#${user.clientId}`, version: 1, + letterVersion: 'PDF_PROOFING', }; const updateTime = '2025-03-12T08:41:33.666Z'; @@ -529,6 +530,7 @@ describe('templateClient', () => { proofingEnabled: expected, owner: `CLIENT#${user.clientId}`, version: 1, + letterVersion: 'PDF_PROOFING', }; const updateTime = '2025-03-12T08:41:33.666Z'; @@ -950,6 +952,7 @@ describe('templateClient', () => { updatedAt: new Date().toISOString(), templateStatus: 'PENDING_VALIDATION', lockNumber: 1, + letterVersion: 'PDF_PROOFING', }; const initialCreatedTemplate: DatabaseTemplate = { @@ -1047,6 +1050,7 @@ describe('templateClient', () => { updatedAt: new Date().toISOString(), templateStatus: 'PENDING_VALIDATION', lockNumber: 1, + letterVersion: 'PDF_PROOFING', }; const initialCreatedTemplate: DatabaseTemplate = { @@ -1739,6 +1743,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', files: {} as LetterFiles, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -1778,6 +1783,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', files: {} as LetterFiles, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -1995,6 +2001,7 @@ describe('templateClient', () => { lockNumber: 1, language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', campaignId: 'campaign-id', files: { pdfTemplate: { @@ -2053,6 +2060,7 @@ describe('templateClient', () => { lockNumber: 1, language: 'en', letterType: 'x0', + letterVersion: 'PDF_PROOFING', campaignId: 'campaign-id', files: { pdfTemplate: { @@ -2413,6 +2421,7 @@ describe('templateClient', () => { id: templateId, language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', name: templateName, personalisationParameters, templateStatus: 'SUBMITTED', @@ -2499,6 +2508,7 @@ describe('templateClient', () => { id: templateId, language: 'en', letterType: 'x1', + letterVersion: 'PDF_PROOFING', name: templateName, personalisationParameters, templateStatus: 'SUBMITTED', diff --git a/lambdas/backend-api/src/__tests__/fixtures/template.ts b/lambdas/backend-api/src/__tests__/fixtures/template.ts index acf8053ce..dcfa394d1 100644 --- a/lambdas/backend-api/src/__tests__/fixtures/template.ts +++ b/lambdas/backend-api/src/__tests__/fixtures/template.ts @@ -4,7 +4,7 @@ import type { NhsAppProperties, SmsProperties, TemplateDto, - UploadLetterProperties, + CreatePdfProofingLetterProperties, } from 'nhs-notify-backend-client'; import { WithAttachments } from '../../infra/template-repository'; import { DatabaseTemplate } from 'nhs-notify-web-template-management-utils'; @@ -30,7 +30,7 @@ const nhsAppProperties: NhsAppProperties = { templateType: 'NHS_APP', }; -const letterProperties: WithAttachments = { +const letterProperties: WithAttachments = { templateType: 'LETTER', letterType: 'x0', language: 'en', @@ -134,8 +134,10 @@ export const makeSmsTemplate = ( }; export const makeLetterTemplate = ( - overrides: Partial> = {} -): TemplateFixture => { + overrides: Partial< + TemplateDto & WithAttachments + > = {} +): TemplateFixture => { const createUpdateTemplate = { ...createTemplateProperties, ...letterProperties, @@ -144,6 +146,7 @@ export const makeLetterTemplate = ( const dtoTemplate = { ...createUpdateTemplate, ...dtoProperties, + letterVersion: 'PDF_PROOFING' as const, }; const databaseTemplate = { ...dtoTemplate, diff --git a/lambdas/backend-api/src/infra/template-repository/repository.ts b/lambdas/backend-api/src/infra/template-repository/repository.ts index d07aa8635..00a3cb35e 100644 --- a/lambdas/backend-api/src/infra/template-repository/repository.ts +++ b/lambdas/backend-api/src/infra/template-repository/repository.ts @@ -89,6 +89,7 @@ export class TemplateRepository { createdBy: this.internalUserKey(user), ...(template.templateType === 'LETTER' && { campaignId, + letterVersion: 'PDF_PROOFING' as const, }), lockNumber: 0, }; diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index 814fbd944..82d8cc246 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -1,7 +1,10 @@ import { - $UploadLetterProperties, + $AuthoringLetterProperties, + $CreatePdfProofingLetterProperties, $CreateUpdateNonLetter, $CreateUpdateTemplate, + $LetterProperties, + $PdfProofingLetterProperties, $TemplateFilter, } from '../../schemas'; import type { CreateUpdateTemplate } from '../../types/generated'; @@ -199,7 +202,7 @@ describe('Template schemas', () => { }); test('Letter template fields - should fail validation, when no letterType', async () => { - const result = $UploadLetterProperties.safeParse({ + const result = $CreatePdfProofingLetterProperties.safeParse({ name: 'Test Template', campaignId: 'campaign-id', templateType: 'LETTER', @@ -251,6 +254,159 @@ describe('Template schemas', () => { }); }); + describe('$PdfProofingLetterProperties', () => { + const validPdfProofingLetter = { + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + letterVersion: 'PDF_PROOFING', + files: { + pdfTemplate: { + fileName: 'test.pdf', + currentVersion: '1', + virusScanStatus: 'PASSED', + }, + }, + }; + + test('should pass validation for valid PDF_PROOFING letter', () => { + const result = $PdfProofingLetterProperties.safeParse( + validPdfProofingLetter + ); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validPdfProofingLetter); + }); + + test('should fail validation when letterVersion is not PDF_PROOFING', () => { + const result = $PdfProofingLetterProperties.safeParse({ + ...validPdfProofingLetter, + letterVersion: 'AUTHORING', + }); + + expect(result.success).toBe(false); + }); + }); + + describe('$AuthoringLetterProperties', () => { + const validAuthoringLetter = { + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + letterVersion: 'AUTHORING', + letterVariantId: 'variant-123', + sidesCount: 2, + }; + + test('should pass validation for valid AUTHORING letter', () => { + const result = $AuthoringLetterProperties.safeParse(validAuthoringLetter); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validAuthoringLetter); + }); + + test('should fail validation when letterVersion is not AUTHORING', () => { + const result = $AuthoringLetterProperties.safeParse({ + ...validAuthoringLetter, + letterVersion: 'PDF_PROOFING', + }); + + expect(result.success).toBe(false); + }); + + test('should fail validation when required fields are missing', () => { + const result = $AuthoringLetterProperties.safeParse({ + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + letterVersion: 'AUTHORING', + }); + + expect(result.success).toBe(false); + expect(result.error?.flatten().fieldErrors).toEqual( + expect.objectContaining({ + letterVariantId: expect.any(Array), + sidesCount: expect.any(Array), + }) + ); + }); + }); + + describe('$LetterProperties', () => { + const validLetterWithVersion = { + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + letterVersion: 'PDF_PROOFING', + files: { + pdfTemplate: { + fileName: 'test.pdf', + currentVersion: '1', + virusScanStatus: 'PASSED', + }, + }, + }; + + test('should pass validation when letterVersion is provided', () => { + const result = $LetterProperties.safeParse(validLetterWithVersion); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validLetterWithVersion); + }); + + test('should default letterVersion to PDF_PROOFING when not provided', () => { + const letterWithoutVersion = { + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + files: { + pdfTemplate: { + fileName: 'test.pdf', + currentVersion: '1', + virusScanStatus: 'PASSED', + }, + }, + }; + + const result = $LetterProperties.safeParse(letterWithoutVersion); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ + ...letterWithoutVersion, + letterVersion: 'PDF_PROOFING', + }); + }); + + test('should not add letterVersion for non-LETTER templates', () => { + const emailTemplate = { + templateType: 'EMAIL', + subject: 'Test', + message: 'Hello', + }; + + const result = $LetterProperties.safeParse(emailTemplate); + + // Should fail validation since it's not a letter + expect(result.success).toBe(false); + }); + + test('should pass validation for AUTHORING letter', () => { + const authoringLetter = { + templateType: 'LETTER', + letterType: 'x0', + language: 'en', + letterVersion: 'AUTHORING', + letterVariantId: 'variant-123', + sidesCount: 2, + }; + + const result = $LetterProperties.safeParse(authoringLetter); + + expect(result.success).toBe(true); + expect(result.data).toEqual(authoringLetter); + }); + }); + describe('$TemplateFilter', () => { test.each(['templateType', 'language', 'letterType', 'excludeLanguage'])( '$TemplateFilter should fail when unknown $filter field is provided', diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index c51ffb5e3..454f80e05 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -1,13 +1,15 @@ import { z } from 'zod/v4'; import { + AuthoringLetterProperties, BaseTemplate, - UploadLetterProperties, + CreatePdfProofingLetterProperties, CreateUpdateTemplate, EmailProperties, VersionedFileDetails, ProofFileDetails, LetterFiles, LetterProperties, + PdfProofingLetterProperties, NhsAppProperties, SmsProperties, TemplateDto, @@ -89,21 +91,57 @@ export const $BaseLetterTemplateProperties = z.object({ language: z.enum(LANGUAGE_LIST), }); -export const $UploadLetterProperties = schemaFor()( - z.object({ - ...$BaseLetterTemplateProperties.shape, - campaignId: z.string(), - }) -); - -export const $LetterProperties = schemaFor()( - z.object({ - ...$BaseLetterTemplateProperties.shape, - files: $LetterFiles, - personalisationParameters: z.array(z.string()).optional(), - proofingEnabled: z.boolean().optional(), - supplierReferences: z.record(z.string(), z.string()).optional(), - }) +export const $CreatePdfProofingLetterProperties = + schemaFor()( + z.object({ + ...$BaseLetterTemplateProperties.shape, + campaignId: z.string(), + }) + ); + +export const $PdfProofingLetterProperties = + schemaFor()( + z.object({ + ...$BaseLetterTemplateProperties.shape, + files: $LetterFiles, + letterVersion: z.literal('PDF_PROOFING'), + personalisationParameters: z.array(z.string()).optional(), + proofingEnabled: z.boolean().optional(), + supplierReferences: z.record(z.string(), z.string()).optional(), + }) + ); + +export const $AuthoringLetterProperties = + schemaFor()( + z.object({ + ...$BaseLetterTemplateProperties.shape, + letterVariantId: z.string(), + letterVersion: z.literal('AUTHORING'), + sidesCount: z.number().int(), + }) + ); + +// Default letterVersion to PDF_PROOFING for existing letters in the database +// that were created before letterVersion was introduced +const addDefaultLetterVersion = (val: unknown) => { + if ( + typeof val === 'object' && + val !== null && + 'templateType' in val && + val.templateType === 'LETTER' && + !('letterVersion' in val) + ) { + return { ...val, letterVersion: 'PDF_PROOFING' }; + } + return val; +}; + +export const $LetterProperties: z.ZodType = z.preprocess( + addDefaultLetterVersion, + z.discriminatedUnion('letterVersion', [ + $PdfProofingLetterProperties, + $AuthoringLetterProperties, + ]) ); export const $BaseTemplateSchema = schemaFor()( @@ -128,7 +166,7 @@ export const $CreateUpdateTemplate = schemaFor()( $BaseTemplateSchema.extend($NhsAppProperties.shape), $BaseTemplateSchema.extend($EmailProperties.shape), $BaseTemplateSchema.extend($SmsProperties.shape), - $BaseTemplateSchema.extend($UploadLetterProperties.shape), + $BaseTemplateSchema.extend($CreatePdfProofingLetterProperties.shape), ]) ); @@ -176,15 +214,18 @@ const $BaseTemplateDto = schemaFor< }) ); -export const $TemplateDto = schemaFor< - TemplateDto, - Omit ->()( - z.discriminatedUnion('templateType', [ - $BaseTemplateDto.extend($NhsAppProperties.shape), - $BaseTemplateDto.extend($EmailProperties.shape), - $BaseTemplateDto.extend($SmsProperties.shape), - $BaseTemplateDto.extend($LetterProperties.shape), +export const $TemplateDto: z.ZodType = z.preprocess( + addDefaultLetterVersion, + z.union([ + z.discriminatedUnion('templateType', [ + $BaseTemplateDto.extend($NhsAppProperties.shape), + $BaseTemplateDto.extend($EmailProperties.shape), + $BaseTemplateDto.extend($SmsProperties.shape), + ]), + z.discriminatedUnion('letterVersion', [ + $BaseTemplateDto.extend($PdfProofingLetterProperties.shape), + $BaseTemplateDto.extend($AuthoringLetterProperties.shape), + ]), ]) ); diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index 906f037c3..255f16b02 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -125,6 +125,10 @@ export type CountSuccess = { statusCode: number; }; +export type CreatePdfProofingLetterProperties = BaseLetterTemplateProperties & { + campaignId: string; +}; + export type CreateRoutingConfig = { campaignId: string; cascade: Array; @@ -183,8 +187,6 @@ export type Language = | 'ur' | 'zh'; -export type LetterVersion = 'AUTHORING' | 'PDF_PROOFING'; - export type LetterFiles = { pdfTemplate: VersionedFileDetails; proofs?: { @@ -194,12 +196,17 @@ export type LetterFiles = { }; export type LetterProperties = - | ({ - letterVersion: 'AUTHORING'; - } & AuthoringLetterProperties) - | ({ - letterVersion: 'PDF_PROOFING'; - } & PdfProofingLetterProperties); + | AuthoringLetterProperties + | PdfProofingLetterProperties; + +export type LetterType = 'q4' | 'x0' | 'x1'; + +export type LetterVersion = 'AUTHORING' | 'PDF_PROOFING'; + +export type NhsAppProperties = { + message: string; + templateType: 'NHS_APP'; +}; export type PdfProofingLetterProperties = BaseLetterTemplateProperties & { files: LetterFiles; @@ -211,13 +218,6 @@ export type PdfProofingLetterProperties = BaseLetterTemplateProperties & { }; }; -export type LetterType = 'q4' | 'x0' | 'x1'; - -export type NhsAppProperties = { - message: string; - templateType: 'NHS_APP'; -}; - export type ProofFileDetails = { fileName: string; supplier: string; @@ -303,10 +303,6 @@ export type UpdateRoutingConfig = unknown & { name?: string; }; -export type CreatePdfProofingLetterProperties = BaseLetterTemplateProperties & { - campaignId: string; -}; - export type UploadLetterTemplate = { letterPdf?: Blob | File; template?: CreateUpdateTemplate; diff --git a/tests/accessibility/pa11y-setup.ts b/tests/accessibility/pa11y-setup.ts index e8cd414b0..6643881ff 100644 --- a/tests/accessibility/pa11y-setup.ts +++ b/tests/accessibility/pa11y-setup.ts @@ -25,6 +25,7 @@ const generateLetterTemplateData = ( id: randomUUID(), templateType: 'LETTER', letterType: 'x0', + letterVersion: 'PDF_PROOFING', language: 'en', createdAt: now, updatedAt: now, diff --git a/utils/utils/jest.config.ts b/utils/utils/jest.config.ts index b4549dc91..322eab4b7 100644 --- a/utils/utils/jest.config.ts +++ b/utils/utils/jest.config.ts @@ -53,6 +53,7 @@ const utilsJestConfig = { coveragePathIgnorePatterns: [ ...(baseJestConfig.coveragePathIgnorePatterns ?? []), 'zod-validators.ts', + 'jest.config.ts', ], }; diff --git a/utils/utils/src/__tests__/email-client.test.ts b/utils/utils/src/__tests__/email-client.test.ts index 251cb573b..949e94fac 100644 --- a/utils/utils/src/__tests__/email-client.test.ts +++ b/utils/utils/src/__tests__/email-client.test.ts @@ -3,7 +3,7 @@ import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'; import { Logger } from 'nhs-notify-web-template-management-utils/logger'; import { EmailClient } from '../email-client'; import { TemplateDto } from 'nhs-notify-backend-client'; -import { LetterTemplate } from '../types'; +import { PdfProofingLetterTemplate } from '../types'; describe('EmailClient', () => { const recipientEmails = { @@ -73,9 +73,10 @@ describe('EmailClient', () => { }); describe('template-submitted email', () => { - const mockTemplate = mockDeep({ + const mockTemplate = mockDeep({ id: 'template-id', templateType: 'LETTER', + letterVersion: 'PDF_PROOFING', files: { proofs: { proof1: { fileName: 'proof1.pdf', supplier: 'supplier1' }, @@ -186,6 +187,7 @@ describe('EmailClient', () => { const template = mockDeep({ id: 'template-id', templateType: 'LETTER', + letterVersion: 'PDF_PROOFING', files: { proofs: undefined, }, diff --git a/utils/utils/src/__tests__/types.test.ts b/utils/utils/src/__tests__/types.test.ts new file mode 100644 index 000000000..6f67fca96 --- /dev/null +++ b/utils/utils/src/__tests__/types.test.ts @@ -0,0 +1,60 @@ +import { + assertPdfProofingLetter, + PdfProofingLetterTemplate, + AuthoringLetterTemplate, + UnexpectedLetterVersionError, +} from '../types'; + +describe('types', () => { + describe('UnexpectedLetterVersionError', () => { + it('creates error with correct message and name', () => { + const error = new UnexpectedLetterVersionError('AUTHORING'); + + expect(error.message).toBe('ERR_UNEXPECTED_LETTER_VERSION: AUTHORING'); + expect(error.name).toBe('UnexpectedLetterVersionError'); + expect(error).toBeInstanceOf(Error); + }); + }); + + describe('assertPdfProofingLetter', () => { + const basePdfProofingTemplate: PdfProofingLetterTemplate = { + id: 'template-id', + name: 'Test Template', + templateType: 'LETTER', + templateStatus: 'NOT_YET_SUBMITTED', + letterVersion: 'PDF_PROOFING', + letterType: 'x0', + language: 'en', + files: { + pdfTemplate: { + fileName: 'test.pdf', + currentVersion: '1', + virusScanStatus: 'PASSED', + }, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + lockNumber: 0, + }; + + it('returns the template when letterVersion is PDF_PROOFING', () => { + const result = assertPdfProofingLetter(basePdfProofingTemplate); + + expect(result).toBe(basePdfProofingTemplate); + }); + + it('throws UnexpectedLetterVersionError when letterVersion is not PDF_PROOFING', () => { + const authoringTemplate = { + ...basePdfProofingTemplate, + letterVersion: 'AUTHORING', + } as unknown as AuthoringLetterTemplate; + + expect(() => assertPdfProofingLetter(authoringTemplate)).toThrow( + UnexpectedLetterVersionError + ); + expect(() => assertPdfProofingLetter(authoringTemplate)).toThrow( + 'ERR_UNEXPECTED_LETTER_VERSION: AUTHORING' + ); + }); + }); +}); diff --git a/utils/utils/src/email-client.ts b/utils/utils/src/email-client.ts index aab4dc7d0..f10f4f603 100644 --- a/utils/utils/src/email-client.ts +++ b/utils/utils/src/email-client.ts @@ -3,7 +3,10 @@ import { createMimeMessage } from 'mimetext'; import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'; import { TemplateDto } from 'nhs-notify-backend-client'; import { Logger } from 'nhs-notify-web-template-management-utils/logger'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { + LetterTemplate, + assertPdfProofingLetter, +} from 'nhs-notify-web-template-management-utils'; import Handlebars from 'handlebars'; import { readFileSync } from 'node:fs'; import path from 'node:path'; @@ -193,10 +196,11 @@ export class EmailClient { return; } + const pdfProofingLetter = assertPdfProofingLetter(template); const proofsBySupplier: Record = {}; for (const { supplier, fileName } of Object.values( - template.files.proofs ?? {} + pdfProofingLetter.files.proofs ?? {} )) { const proofFilenames = [...(proofsBySupplier[supplier] ?? []), fileName]; proofsBySupplier[supplier] = proofFilenames; @@ -204,7 +208,7 @@ export class EmailClient { for (const [supplier, proofFilenames] of Object.entries(proofsBySupplier)) { await this.sendTemplateSubmittedEmailToSupplier( - template, + pdfProofingLetter, supplier, proofFilenames ); diff --git a/utils/utils/src/types.ts b/utils/utils/src/types.ts index c109f9aa3..0231bc523 100644 --- a/utils/utils/src/types.ts +++ b/utils/utils/src/types.ts @@ -1,9 +1,13 @@ import type { GuardDutyScanResultNotificationEventDetail } from 'aws-lambda'; import { + AuthoringLetterProperties, + BaseCreatedTemplate, CreateUpdateTemplate, Language, LetterFiles, LetterType, + LetterVersion, + PdfProofingLetterProperties, TemplateDto, TemplateStatus, TemplateType, @@ -53,7 +57,31 @@ export type EmailTemplate = Extract; export type SMSTemplate = Extract; -export type LetterTemplate = Extract; +export type PdfProofingLetterTemplate = BaseCreatedTemplate & + PdfProofingLetterProperties; + +export type AuthoringLetterTemplate = BaseCreatedTemplate & + AuthoringLetterProperties; + +export type LetterTemplate = + | PdfProofingLetterTemplate + | AuthoringLetterTemplate; + +export class UnexpectedLetterVersionError extends Error { + constructor(letterVersion: string) { + super(`ERR_UNEXPECTED_LETTER_VERSION: ${letterVersion}`); + this.name = 'UnexpectedLetterVersionError'; + } +} + +export const assertPdfProofingLetter = ( + template: LetterTemplate +): PdfProofingLetterTemplate => { + if (template.letterVersion !== 'PDF_PROOFING') { + throw new UnexpectedLetterVersionError(template.letterVersion); + } + return template; +}; export type TemplateFormState = FormState & T; @@ -117,6 +145,7 @@ export type DatabaseTemplate = { id: string; language?: Language; letterType?: LetterType; + letterVersion?: LetterVersion; lockNumber?: number; message?: string; name: string; diff --git a/utils/utils/src/zod-validators.ts b/utils/utils/src/zod-validators.ts index 185f8aa39..f32954558 100644 --- a/utils/utils/src/zod-validators.ts +++ b/utils/utils/src/zod-validators.ts @@ -1,11 +1,11 @@ import { z } from 'zod'; import { - $UploadLetterProperties, + $CreatePdfProofingLetterProperties, $CreateUpdateTemplate, $EmailProperties, $Language, $LetterFiles, - $LetterProperties, + $PdfProofingLetterProperties, $LetterType, $NhsAppProperties, $SmsProperties, @@ -74,11 +74,11 @@ export const $SubmittedSMSTemplate = z.intersection( export const $UploadLetterTemplate = z.intersection( $CreateUpdateTemplate, - $UploadLetterProperties + $CreatePdfProofingLetterProperties ); export const $LetterTemplate = z.intersection( $TemplateDto, - $LetterProperties.extend({ files: $LetterFiles }) + $PdfProofingLetterProperties.extend({ files: $LetterFiles }) ); export const $SubmittedLetterTemplate = z.intersection( $SubmittedTemplate, From c57e39cd72f1c9b613260baf85408ec6aa0b5506 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 00:05:26 +0000 Subject: [PATCH 03/74] fix template dto --- .../src/__tests__/schemas/template.test.ts | 103 ++++++++++++++++++ .../backend-client/src/schemas/template.ts | 36 +++--- 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index 82d8cc246..ba0b68f45 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -5,6 +5,7 @@ import { $CreateUpdateTemplate, $LetterProperties, $PdfProofingLetterProperties, + $TemplateDto, $TemplateFilter, } from '../../schemas'; import type { CreateUpdateTemplate } from '../../types/generated'; @@ -498,4 +499,106 @@ describe('Template schemas', () => { expect(result.data).toEqual(filter); }); }); + + describe('$TemplateDto', () => { + test('should fail validation when letterVersion is not provided for LETTER template', () => { + // Legacy records without letterVersion must have it added by the repository layer + // before validation + const letterWithoutVersion = { + id: 'test-id', + name: 'Test Letter', + templateType: 'LETTER', + templateStatus: 'NOT_YET_SUBMITTED', + letterType: 'x0', + language: 'en', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + files: { + pdfTemplate: { + currentVersion: '1', + fileName: 'test.pdf', + virusScanStatus: 'PASSED', + }, + }, + }; + + const result = $TemplateDto.safeParse(letterWithoutVersion); + + expect(result.success).toBe(false); + }); + + test('should pass validation for LETTER template with letterVersion provided', () => { + const letterWithVersion = { + id: 'test-id', + name: 'Test Letter', + templateType: 'LETTER', + templateStatus: 'NOT_YET_SUBMITTED', + letterType: 'x0', + language: 'en', + letterVersion: 'PDF_PROOFING', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + files: { + pdfTemplate: { + currentVersion: '1', + fileName: 'test.pdf', + virusScanStatus: 'PASSED', + }, + }, + }; + + const result = $TemplateDto.safeParse(letterWithVersion); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ + ...letterWithVersion, + lockNumber: 0, + }); + }); + + test('should pass validation for AUTHORING letter template', () => { + const authoringLetter = { + id: 'test-id', + name: 'Test Authoring Letter', + templateType: 'LETTER', + templateStatus: 'NOT_YET_SUBMITTED', + letterType: 'x0', + language: 'en', + letterVersion: 'AUTHORING', + letterVariantId: 'variant-123', + sidesCount: 2, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }; + + const result = $TemplateDto.safeParse(authoringLetter); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ + ...authoringLetter, + lockNumber: 0, + }); + }); + + test('should pass validation for non-LETTER templates', () => { + const emailTemplate = { + id: 'test-id', + name: 'Test Email', + templateType: 'EMAIL', + templateStatus: 'NOT_YET_SUBMITTED', + subject: 'Test Subject', + message: 'Test message', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }; + + const result = $TemplateDto.safeParse(emailTemplate); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ + ...emailTemplate, + lockNumber: 0, + }); + }); + }); }); diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index 454f80e05..b45e84ef3 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -12,7 +12,6 @@ import { PdfProofingLetterProperties, NhsAppProperties, SmsProperties, - TemplateDto, LetterType, Language, BaseCreatedTemplate, @@ -214,21 +213,30 @@ const $BaseTemplateDto = schemaFor< }) ); -export const $TemplateDto: z.ZodType = z.preprocess( - addDefaultLetterVersion, - z.union([ - z.discriminatedUnion('templateType', [ - $BaseTemplateDto.extend($NhsAppProperties.shape), - $BaseTemplateDto.extend($EmailProperties.shape), - $BaseTemplateDto.extend($SmsProperties.shape), - ]), - z.discriminatedUnion('letterVersion', [ - $BaseTemplateDto.extend($PdfProofingLetterProperties.shape), - $BaseTemplateDto.extend($AuthoringLetterProperties.shape), - ]), - ]) +const $PdfProofingLetterTemplateDto = $BaseTemplateDto.extend( + $PdfProofingLetterProperties.shape ); +const $AuthoringLetterTemplateDto = $BaseTemplateDto.extend( + $AuthoringLetterProperties.shape +); + +// Inner discriminated union for LETTER templates by letterVersion +const $LetterTemplateDto = z.discriminatedUnion('letterVersion', [ + $PdfProofingLetterTemplateDto, + $AuthoringLetterTemplateDto, +]); + +// Outer discriminated union by templateType +// Note: For legacy LETTER records without letterVersion, the application/repository layer +// must add letterVersion: 'PDF_PROOFING' before validation +export const $TemplateDto = z.discriminatedUnion('templateType', [ + $BaseTemplateDto.extend($NhsAppProperties.shape), + $BaseTemplateDto.extend($EmailProperties.shape), + $BaseTemplateDto.extend($SmsProperties.shape), + $LetterTemplateDto, +]); + export const $TemplateFilter = z.object({ templateStatus: $TemplateStatusFilter.optional(), templateType: $TemplateType.optional(), From 3dec0b2f62d98f6d5ea3a48bbce6c1c28d710985 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 00:14:12 +0000 Subject: [PATCH 04/74] zod default --- .../src/__tests__/schemas/template.test.ts | 11 +++++++---- lambdas/backend-client/src/schemas/template.ts | 17 +++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index ba0b68f45..04e77b7d3 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -501,9 +501,7 @@ describe('Template schemas', () => { }); describe('$TemplateDto', () => { - test('should fail validation when letterVersion is not provided for LETTER template', () => { - // Legacy records without letterVersion must have it added by the repository layer - // before validation + test('should default letterVersion to PDF_PROOFING when not provided for LETTER template', () => { const letterWithoutVersion = { id: 'test-id', name: 'Test Letter', @@ -524,7 +522,12 @@ describe('Template schemas', () => { const result = $TemplateDto.safeParse(letterWithoutVersion); - expect(result.success).toBe(false); + expect(result.success).toBe(true); + expect(result.data).toEqual({ + ...letterWithoutVersion, + lockNumber: 0, + letterVersion: 'PDF_PROOFING', + }); }); test('should pass validation for LETTER template with letterVersion provided', () => { diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index b45e84ef3..b822aaf9f 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -213,14 +213,21 @@ const $BaseTemplateDto = schemaFor< }) ); -const $PdfProofingLetterTemplateDto = $BaseTemplateDto.extend( - $PdfProofingLetterProperties.shape -); - const $AuthoringLetterTemplateDto = $BaseTemplateDto.extend( $AuthoringLetterProperties.shape ); +// For DTO parsing, accept undefined letterVersion and default to 'PDF_PROOFING' +// for legacy LETTER records created before letterVersion was introduced +const $DefaultablePdfProofingLetterVersion = z + .union([z.undefined(), z.literal('PDF_PROOFING')]) + .transform((val) => val ?? ('PDF_PROOFING' as const)); + +const $PdfProofingLetterTemplateDto = $BaseTemplateDto.extend({ + ...$PdfProofingLetterProperties.shape, + letterVersion: $DefaultablePdfProofingLetterVersion, +}); + // Inner discriminated union for LETTER templates by letterVersion const $LetterTemplateDto = z.discriminatedUnion('letterVersion', [ $PdfProofingLetterTemplateDto, @@ -228,8 +235,6 @@ const $LetterTemplateDto = z.discriminatedUnion('letterVersion', [ ]); // Outer discriminated union by templateType -// Note: For legacy LETTER records without letterVersion, the application/repository layer -// must add letterVersion: 'PDF_PROOFING' before validation export const $TemplateDto = z.discriminatedUnion('templateType', [ $BaseTemplateDto.extend($NhsAppProperties.shape), $BaseTemplateDto.extend($EmailProperties.shape), From 8338bc5f09fb1f82fed06b77ada67d04c1333a11 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 07:25:20 +0000 Subject: [PATCH 05/74] cleanup --- .../backend-client/src/schemas/template.ts | 44 ++++++------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index b822aaf9f..53701445e 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -8,7 +8,6 @@ import { VersionedFileDetails, ProofFileDetails, LetterFiles, - LetterProperties, PdfProofingLetterProperties, NhsAppProperties, SmsProperties, @@ -120,28 +119,19 @@ export const $AuthoringLetterProperties = }) ); -// Default letterVersion to PDF_PROOFING for existing letters in the database -// that were created before letterVersion was introduced -const addDefaultLetterVersion = (val: unknown) => { - if ( - typeof val === 'object' && - val !== null && - 'templateType' in val && - val.templateType === 'LETTER' && - !('letterVersion' in val) - ) { - return { ...val, letterVersion: 'PDF_PROOFING' }; - } - return val; -}; - -export const $LetterProperties: z.ZodType = z.preprocess( - addDefaultLetterVersion, - z.discriminatedUnion('letterVersion', [ - $PdfProofingLetterProperties, - $AuthoringLetterProperties, - ]) -); +const $DefaultablePdfProofingLetterVersion = z + .union([z.undefined(), z.literal('PDF_PROOFING')]) + .transform((val) => val ?? ('PDF_PROOFING' as const)); + +const $PdfProofingLetterPropertiesWithDefault = z.object({ + ...$PdfProofingLetterProperties.shape, + letterVersion: $DefaultablePdfProofingLetterVersion, +}); + +export const $LetterProperties = z.discriminatedUnion('letterVersion', [ + $PdfProofingLetterPropertiesWithDefault, + $AuthoringLetterProperties, +]); export const $BaseTemplateSchema = schemaFor()( z.object({ @@ -217,24 +207,16 @@ const $AuthoringLetterTemplateDto = $BaseTemplateDto.extend( $AuthoringLetterProperties.shape ); -// For DTO parsing, accept undefined letterVersion and default to 'PDF_PROOFING' -// for legacy LETTER records created before letterVersion was introduced -const $DefaultablePdfProofingLetterVersion = z - .union([z.undefined(), z.literal('PDF_PROOFING')]) - .transform((val) => val ?? ('PDF_PROOFING' as const)); - const $PdfProofingLetterTemplateDto = $BaseTemplateDto.extend({ ...$PdfProofingLetterProperties.shape, letterVersion: $DefaultablePdfProofingLetterVersion, }); -// Inner discriminated union for LETTER templates by letterVersion const $LetterTemplateDto = z.discriminatedUnion('letterVersion', [ $PdfProofingLetterTemplateDto, $AuthoringLetterTemplateDto, ]); -// Outer discriminated union by templateType export const $TemplateDto = z.discriminatedUnion('templateType', [ $BaseTemplateDto.extend($NhsAppProperties.shape), $BaseTemplateDto.extend($EmailProperties.shape), From c0831b400f61856369dfd115bd4b7bf16851b392 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 08:18:54 +0000 Subject: [PATCH 06/74] rename letter version --- .../__tests__/app/copy-template/page.test.tsx | 2 +- .../app/preview-letter-template/page.test.tsx | 2 +- .../page.test.tsx | 2 +- .../request-proof-of-template/page.test.tsx | 4 ++-- .../LetterTemplateForm/server-action.test.ts | 4 ++-- .../forms/RequestProof/server-action.test.ts | 2 +- .../SubmitTemplate/server-action.test.ts | 2 +- .../molecules/MessageTemplates.test.tsx | 6 ++--- .../PreviewSubmittedTemplate.test.tsx | 2 +- .../molecules/PreviewTemplateDetails.test.tsx | 10 ++++----- .../organisms/PreviewLetterTemplate.test.tsx | 22 +++++++++---------- frontend/src/__tests__/helpers/helpers.ts | 4 ++-- .../src/__tests__/utils/form-actions.test.ts | 6 ++--- frontend/src/utils/routing-utils.ts | 2 +- .../modules/backend-api/spec.tmpl.json | 4 ++-- .../src/__tests__/api/list.test.ts | 4 ++-- .../src/__tests__/api/proof.test.ts | 2 +- .../src/__tests__/api/upload-letter.test.ts | 4 ++-- .../src/__tests__/app/template-client.test.ts | 20 ++++++++--------- .../src/__tests__/fixtures/template.ts | 2 +- .../infra/template-repository/repository.ts | 2 +- .../src/__tests__/schemas/template.test.ts | 20 ++++++++--------- .../backend-client/src/schemas/template.ts | 6 ++--- .../src/types/generated/types.gen.ts | 4 ++-- tests/accessibility/pa11y-setup.ts | 2 +- .../utils/src/__tests__/email-client.test.ts | 4 ++-- utils/utils/src/__tests__/types.test.ts | 6 ++--- utils/utils/src/types.ts | 2 +- 28 files changed, 76 insertions(+), 76 deletions(-) diff --git a/frontend/src/__tests__/app/copy-template/page.test.tsx b/frontend/src/__tests__/app/copy-template/page.test.tsx index a830c104a..1a4b4c8f0 100644 --- a/frontend/src/__tests__/app/copy-template/page.test.tsx +++ b/frontend/src/__tests__/app/copy-template/page.test.tsx @@ -40,7 +40,7 @@ describe('CopyTemplatePage', () => { lockNumber: 1, letterType: 'q4', language: 'fr', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx index e7b0b74c2..221375e57 100644 --- a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx @@ -44,7 +44,7 @@ const templateDTO = { id: 'template-id', language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', name: 'template-name', templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', diff --git a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx index 8b8b59ddd..010973e29 100644 --- a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx @@ -42,7 +42,7 @@ describe('PreviewSubmittedLetterTemplatePage', () => { id: 'template-id', language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', name: 'template-name', templateStatus: 'SUBMITTED', templateType: 'LETTER', diff --git a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx index 6b732c9d1..8cbc5bcb0 100644 --- a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx +++ b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx @@ -41,7 +41,7 @@ describe('RequestProofPage', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', proofingEnabled: true, files: { @@ -145,7 +145,7 @@ describe('RequestProofPage', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', proofingEnabled: false, files: { diff --git a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts index 25b3e2773..b54f20b2d 100644 --- a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts @@ -51,7 +51,7 @@ describe('UploadLetterTemplate server actions', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', files: { pdfTemplate: { @@ -114,7 +114,7 @@ describe('UploadLetterTemplate server actions', () => { templateStatus: 'NOT_YET_SUBMITTED', name: 'template-name', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts b/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts index d97077ddd..9729bf3c8 100644 --- a/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/RequestProof/server-action.test.ts @@ -25,7 +25,7 @@ const mockLetterTemplate = (id: string) => createdAt: '2025-01-13T10:19:25.579Z', updatedAt: '2025-01-13T10:19:25.579Z', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts index 6e313750c..172b28a7e 100644 --- a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts @@ -151,7 +151,7 @@ describe('submitTemplate', () => { id: templateId, language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', name: 'template-name', templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', diff --git a/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx b/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx index ca38e62d1..c33f6ba35 100644 --- a/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx +++ b/frontend/src/__tests__/components/molecules/MessageTemplates.test.tsx @@ -44,7 +44,7 @@ const messageTemplatesProps: { name: 'Template 3', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, @@ -63,7 +63,7 @@ const messageTemplatesProps: { name: 'Template 4', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'fr', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, @@ -120,7 +120,7 @@ describe('MessageTemplates component', () => { name: 'Template 1', createdAt: '2021-02-01T00:00:00.000Z', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', updatedAt: '2021-02-01T00:00:00.000Z', lockNumber: 1, diff --git a/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx b/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx index 6fb531417..f90dbbc82 100644 --- a/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewSubmittedTemplate.test.tsx @@ -86,7 +86,7 @@ describe('PreviewSubmittedTemplate component', () => { templateStatus: 'SUBMITTED', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index b8aa0ec0b..3508ed5fd 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -78,7 +78,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PENDING_VALIDATION', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'fr', files: { pdfTemplate: { @@ -111,7 +111,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PENDING_VALIDATION', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'fr', files: { pdfTemplate: { @@ -140,7 +140,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PROOF_AVAILABLE', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -190,7 +190,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'PROOF_AVAILABLE', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -218,7 +218,7 @@ describe('PreviewTemplateDetailsLetter', () => { templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx index 83b7cf0b4..c09fb4c9c 100644 --- a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx +++ b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx @@ -20,7 +20,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'VIRUS_SCAN_FAILED', language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', @@ -49,7 +49,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PENDING_PROOF_REQUEST', letterType: 'q4', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', files: { pdfTemplate: { @@ -78,7 +78,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'WAITING_FOR_PROOF', letterType: 'q4', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', files: { pdfTemplate: { @@ -108,7 +108,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_AVAILABLE', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -146,7 +146,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_AVAILABLE', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -185,7 +185,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'PROOF_APPROVED', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -221,7 +221,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', files: { pdfTemplate: { @@ -258,7 +258,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'VALIDATION_FAILED', language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', @@ -292,7 +292,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'PENDING_UPLOAD', language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', @@ -321,7 +321,7 @@ describe('PreviewLetterTemplate component', () => { templateStatus: 'PENDING_VALIDATION', language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', @@ -349,7 +349,7 @@ describe('PreviewLetterTemplate component', () => { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'ar', files: { pdfTemplate: { diff --git a/frontend/src/__tests__/helpers/helpers.ts b/frontend/src/__tests__/helpers/helpers.ts index b7d7799df..e72585fd4 100644 --- a/frontend/src/__tests__/helpers/helpers.ts +++ b/frontend/src/__tests__/helpers/helpers.ts @@ -58,7 +58,7 @@ export const LETTER_TEMPLATE: PdfProofingLetterTemplate = { templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', language: 'en', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'template.pdf', @@ -78,7 +78,7 @@ export const LARGE_PRINT_LETTER_TEMPLATE: PdfProofingLetterTemplate = { templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x1', language: 'en', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'large-print-template.pdf', diff --git a/frontend/src/__tests__/utils/form-actions.test.ts b/frontend/src/__tests__/utils/form-actions.test.ts index dc5b44a3e..feb944185 100644 --- a/frontend/src/__tests__/utils/form-actions.test.ts +++ b/frontend/src/__tests__/utils/form-actions.test.ts @@ -128,7 +128,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'template.pdf', @@ -189,7 +189,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'template.pdf', @@ -776,7 +776,7 @@ describe('form-actions', () => { name: 'template-name', letterType: 'x1', language: 'ar', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'template.pdf', diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts index bb7c7e43a..258df6581 100644 --- a/frontend/src/utils/routing-utils.ts +++ b/frontend/src/utils/routing-utils.ts @@ -33,7 +33,7 @@ export function isLetterTemplate( export function isPdfProofingLetter( template: LetterTemplate ): template is PdfProofingLetterTemplate { - return template.letterVersion === 'PDF_PROOFING'; + return template.letterVersion === 'PDF'; } /** diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index a1e8df838..275f054e1 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -626,7 +626,7 @@ "LetterVersion": { "enum": [ "AUTHORING", - "PDF_PROOFING" + "PDF" ], "type": "string" }, @@ -660,7 +660,7 @@ }, "letterVersion": { "enum": [ - "PDF_PROOFING" + "PDF" ], "type": "string" }, diff --git a/lambdas/backend-api/src/__tests__/api/list.test.ts b/lambdas/backend-api/src/__tests__/api/list.test.ts index d3261e68e..8224b009e 100644 --- a/lambdas/backend-api/src/__tests__/api/list.test.ts +++ b/lambdas/backend-api/src/__tests__/api/list.test.ts @@ -244,7 +244,7 @@ describe('Template API - List', () => { lockNumber: 1, language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', @@ -306,7 +306,7 @@ describe('Template API - List', () => { lockNumber: 1, language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/lambdas/backend-api/src/__tests__/api/proof.test.ts b/lambdas/backend-api/src/__tests__/api/proof.test.ts index 7c047a2b5..29634e280 100644 --- a/lambdas/backend-api/src/__tests__/api/proof.test.ts +++ b/lambdas/backend-api/src/__tests__/api/proof.test.ts @@ -166,7 +166,7 @@ describe('Template API - request proof', () => { lockNumber: 1, letterType: 'q4', language: 'fr', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'file.pdf', diff --git a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts index 6142de4bf..75ea84e3f 100644 --- a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts +++ b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts @@ -87,7 +87,7 @@ describe('upload-letter', () => { updatedAt: now, lockNumber: 1, templateStatus: 'PENDING_VALIDATION', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: pdfFilename, @@ -163,7 +163,7 @@ describe('upload-letter', () => { lockNumber: 1, clientId, templateStatus: 'PENDING_VALIDATION', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: pdfFilename, diff --git a/lambdas/backend-api/src/__tests__/app/template-client.test.ts b/lambdas/backend-api/src/__tests__/app/template-client.test.ts index 54f8a0cdc..5d6b3eac1 100644 --- a/lambdas/backend-api/src/__tests__/app/template-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/template-client.test.ts @@ -387,7 +387,7 @@ describe('templateClient', () => { templateStatus: 'PENDING_UPLOAD', owner: `CLIENT#${user.clientId}`, version: 1, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }; const updateTime = '2025-03-12T08:41:33.666Z'; @@ -530,7 +530,7 @@ describe('templateClient', () => { proofingEnabled: expected, owner: `CLIENT#${user.clientId}`, version: 1, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }; const updateTime = '2025-03-12T08:41:33.666Z'; @@ -952,7 +952,7 @@ describe('templateClient', () => { updatedAt: new Date().toISOString(), templateStatus: 'PENDING_VALIDATION', lockNumber: 1, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }; const initialCreatedTemplate: DatabaseTemplate = { @@ -1050,7 +1050,7 @@ describe('templateClient', () => { updatedAt: new Date().toISOString(), templateStatus: 'PENDING_VALIDATION', lockNumber: 1, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }; const initialCreatedTemplate: DatabaseTemplate = { @@ -1743,7 +1743,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: {} as LetterFiles, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -1783,7 +1783,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: {} as LetterFiles, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -2001,7 +2001,7 @@ describe('templateClient', () => { lockNumber: 1, language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', campaignId: 'campaign-id', files: { pdfTemplate: { @@ -2060,7 +2060,7 @@ describe('templateClient', () => { lockNumber: 1, language: 'en', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', campaignId: 'campaign-id', files: { pdfTemplate: { @@ -2421,7 +2421,7 @@ describe('templateClient', () => { id: templateId, language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', name: templateName, personalisationParameters, templateStatus: 'SUBMITTED', @@ -2508,7 +2508,7 @@ describe('templateClient', () => { id: templateId, language: 'en', letterType: 'x1', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', name: templateName, personalisationParameters, templateStatus: 'SUBMITTED', diff --git a/lambdas/backend-api/src/__tests__/fixtures/template.ts b/lambdas/backend-api/src/__tests__/fixtures/template.ts index dcfa394d1..7f6ece439 100644 --- a/lambdas/backend-api/src/__tests__/fixtures/template.ts +++ b/lambdas/backend-api/src/__tests__/fixtures/template.ts @@ -146,7 +146,7 @@ export const makeLetterTemplate = ( const dtoTemplate = { ...createUpdateTemplate, ...dtoProperties, - letterVersion: 'PDF_PROOFING' as const, + letterVersion: 'PDF' as const, }; const databaseTemplate = { ...dtoTemplate, diff --git a/lambdas/backend-api/src/infra/template-repository/repository.ts b/lambdas/backend-api/src/infra/template-repository/repository.ts index 00a3cb35e..3f2ce7475 100644 --- a/lambdas/backend-api/src/infra/template-repository/repository.ts +++ b/lambdas/backend-api/src/infra/template-repository/repository.ts @@ -89,7 +89,7 @@ export class TemplateRepository { createdBy: this.internalUserKey(user), ...(template.templateType === 'LETTER' && { campaignId, - letterVersion: 'PDF_PROOFING' as const, + letterVersion: 'PDF' as const, }), lockNumber: 0, }; diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index 04e77b7d3..07a956b6d 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -260,7 +260,7 @@ describe('Template schemas', () => { templateType: 'LETTER', letterType: 'x0', language: 'en', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'test.pdf', @@ -270,7 +270,7 @@ describe('Template schemas', () => { }, }; - test('should pass validation for valid PDF_PROOFING letter', () => { + test('should pass validation for valid PDF letter', () => { const result = $PdfProofingLetterProperties.safeParse( validPdfProofingLetter ); @@ -279,7 +279,7 @@ describe('Template schemas', () => { expect(result.data).toEqual(validPdfProofingLetter); }); - test('should fail validation when letterVersion is not PDF_PROOFING', () => { + test('should fail validation when letterVersion is not PDF', () => { const result = $PdfProofingLetterProperties.safeParse({ ...validPdfProofingLetter, letterVersion: 'AUTHORING', @@ -309,7 +309,7 @@ describe('Template schemas', () => { test('should fail validation when letterVersion is not AUTHORING', () => { const result = $AuthoringLetterProperties.safeParse({ ...validAuthoringLetter, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }); expect(result.success).toBe(false); @@ -338,7 +338,7 @@ describe('Template schemas', () => { templateType: 'LETTER', letterType: 'x0', language: 'en', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'test.pdf', @@ -355,7 +355,7 @@ describe('Template schemas', () => { expect(result.data).toEqual(validLetterWithVersion); }); - test('should default letterVersion to PDF_PROOFING when not provided', () => { + test('should default letterVersion to PDF when not provided', () => { const letterWithoutVersion = { templateType: 'LETTER', letterType: 'x0', @@ -374,7 +374,7 @@ describe('Template schemas', () => { expect(result.success).toBe(true); expect(result.data).toEqual({ ...letterWithoutVersion, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }); }); @@ -501,7 +501,7 @@ describe('Template schemas', () => { }); describe('$TemplateDto', () => { - test('should default letterVersion to PDF_PROOFING when not provided for LETTER template', () => { + test('should default letterVersion to PDF when not provided for LETTER template', () => { const letterWithoutVersion = { id: 'test-id', name: 'Test Letter', @@ -526,7 +526,7 @@ describe('Template schemas', () => { expect(result.data).toEqual({ ...letterWithoutVersion, lockNumber: 0, - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', }); }); @@ -538,7 +538,7 @@ describe('Template schemas', () => { templateStatus: 'NOT_YET_SUBMITTED', letterType: 'x0', language: 'en', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', files: { diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index 53701445e..f64ab4477 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -102,7 +102,7 @@ export const $PdfProofingLetterProperties = z.object({ ...$BaseLetterTemplateProperties.shape, files: $LetterFiles, - letterVersion: z.literal('PDF_PROOFING'), + letterVersion: z.literal('PDF'), personalisationParameters: z.array(z.string()).optional(), proofingEnabled: z.boolean().optional(), supplierReferences: z.record(z.string(), z.string()).optional(), @@ -120,8 +120,8 @@ export const $AuthoringLetterProperties = ); const $DefaultablePdfProofingLetterVersion = z - .union([z.undefined(), z.literal('PDF_PROOFING')]) - .transform((val) => val ?? ('PDF_PROOFING' as const)); + .union([z.undefined(), z.literal('PDF')]) + .transform((val) => val ?? ('PDF' as const)); const $PdfProofingLetterPropertiesWithDefault = z.object({ ...$PdfProofingLetterProperties.shape, diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index 255f16b02..d44345c65 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -201,7 +201,7 @@ export type LetterProperties = export type LetterType = 'q4' | 'x0' | 'x1'; -export type LetterVersion = 'AUTHORING' | 'PDF_PROOFING'; +export type LetterVersion = 'AUTHORING' | 'PDF'; export type NhsAppProperties = { message: string; @@ -210,7 +210,7 @@ export type NhsAppProperties = { export type PdfProofingLetterProperties = BaseLetterTemplateProperties & { files: LetterFiles; - letterVersion: 'PDF_PROOFING'; + letterVersion: 'PDF'; personalisationParameters?: Array; proofingEnabled?: boolean; supplierReferences?: { diff --git a/tests/accessibility/pa11y-setup.ts b/tests/accessibility/pa11y-setup.ts index 6643881ff..6cb9e2357 100644 --- a/tests/accessibility/pa11y-setup.ts +++ b/tests/accessibility/pa11y-setup.ts @@ -25,7 +25,7 @@ const generateLetterTemplateData = ( id: randomUUID(), templateType: 'LETTER', letterType: 'x0', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', language: 'en', createdAt: now, updatedAt: now, diff --git a/utils/utils/src/__tests__/email-client.test.ts b/utils/utils/src/__tests__/email-client.test.ts index 949e94fac..025b46e52 100644 --- a/utils/utils/src/__tests__/email-client.test.ts +++ b/utils/utils/src/__tests__/email-client.test.ts @@ -76,7 +76,7 @@ describe('EmailClient', () => { const mockTemplate = mockDeep({ id: 'template-id', templateType: 'LETTER', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { proofs: { proof1: { fileName: 'proof1.pdf', supplier: 'supplier1' }, @@ -187,7 +187,7 @@ describe('EmailClient', () => { const template = mockDeep({ id: 'template-id', templateType: 'LETTER', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', files: { proofs: undefined, }, diff --git a/utils/utils/src/__tests__/types.test.ts b/utils/utils/src/__tests__/types.test.ts index 6f67fca96..8b08eb7e1 100644 --- a/utils/utils/src/__tests__/types.test.ts +++ b/utils/utils/src/__tests__/types.test.ts @@ -22,7 +22,7 @@ describe('types', () => { name: 'Test Template', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', - letterVersion: 'PDF_PROOFING', + letterVersion: 'PDF', letterType: 'x0', language: 'en', files: { @@ -37,13 +37,13 @@ describe('types', () => { lockNumber: 0, }; - it('returns the template when letterVersion is PDF_PROOFING', () => { + it('returns the template when letterVersion is PDF', () => { const result = assertPdfProofingLetter(basePdfProofingTemplate); expect(result).toBe(basePdfProofingTemplate); }); - it('throws UnexpectedLetterVersionError when letterVersion is not PDF_PROOFING', () => { + it('throws UnexpectedLetterVersionError when letterVersion is not PDF', () => { const authoringTemplate = { ...basePdfProofingTemplate, letterVersion: 'AUTHORING', diff --git a/utils/utils/src/types.ts b/utils/utils/src/types.ts index 0231bc523..1d869cb11 100644 --- a/utils/utils/src/types.ts +++ b/utils/utils/src/types.ts @@ -77,7 +77,7 @@ export class UnexpectedLetterVersionError extends Error { export const assertPdfProofingLetter = ( template: LetterTemplate ): PdfProofingLetterTemplate => { - if (template.letterVersion !== 'PDF_PROOFING') { + if (template.letterVersion !== 'PDF') { throw new UnexpectedLetterVersionError(template.letterVersion); } return template; From 61aae74518ac89f99ca723e50fe8ba318d22972e Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 08:35:23 +0000 Subject: [PATCH 07/74] coverage --- .../molecules/PreviewTemplateDetails.test.tsx | 23 +++++++ .../PreviewTemplateDetailsLetter.tsx | 10 ++-- utils/utils/jest.config.ts | 1 - utils/utils/src/__tests__/types.test.ts | 60 ------------------- utils/utils/src/email-client.ts | 17 +++--- utils/utils/src/types.ts | 16 ----- 6 files changed, 37 insertions(+), 90 deletions(-) delete mode 100644 utils/utils/src/__tests__/types.test.ts diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index 3508ed5fd..3d3075902 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -69,6 +69,29 @@ describe('PreviewTemplateDetailsSms', () => { }); describe('PreviewTemplateDetailsLetter', () => { + it('throws error for AUTHORING letter version', () => { + expect(() => + render( + + ) + ).toThrow('AUTHORING letter version is not implemented'); + }); + it('matches snapshot without proofs', () => { const container = render( virusScanStatus === 'PASSED') diff --git a/utils/utils/jest.config.ts b/utils/utils/jest.config.ts index 322eab4b7..b4549dc91 100644 --- a/utils/utils/jest.config.ts +++ b/utils/utils/jest.config.ts @@ -53,7 +53,6 @@ const utilsJestConfig = { coveragePathIgnorePatterns: [ ...(baseJestConfig.coveragePathIgnorePatterns ?? []), 'zod-validators.ts', - 'jest.config.ts', ], }; diff --git a/utils/utils/src/__tests__/types.test.ts b/utils/utils/src/__tests__/types.test.ts deleted file mode 100644 index 8b08eb7e1..000000000 --- a/utils/utils/src/__tests__/types.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - assertPdfProofingLetter, - PdfProofingLetterTemplate, - AuthoringLetterTemplate, - UnexpectedLetterVersionError, -} from '../types'; - -describe('types', () => { - describe('UnexpectedLetterVersionError', () => { - it('creates error with correct message and name', () => { - const error = new UnexpectedLetterVersionError('AUTHORING'); - - expect(error.message).toBe('ERR_UNEXPECTED_LETTER_VERSION: AUTHORING'); - expect(error.name).toBe('UnexpectedLetterVersionError'); - expect(error).toBeInstanceOf(Error); - }); - }); - - describe('assertPdfProofingLetter', () => { - const basePdfProofingTemplate: PdfProofingLetterTemplate = { - id: 'template-id', - name: 'Test Template', - templateType: 'LETTER', - templateStatus: 'NOT_YET_SUBMITTED', - letterVersion: 'PDF', - letterType: 'x0', - language: 'en', - files: { - pdfTemplate: { - fileName: 'test.pdf', - currentVersion: '1', - virusScanStatus: 'PASSED', - }, - }, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - lockNumber: 0, - }; - - it('returns the template when letterVersion is PDF', () => { - const result = assertPdfProofingLetter(basePdfProofingTemplate); - - expect(result).toBe(basePdfProofingTemplate); - }); - - it('throws UnexpectedLetterVersionError when letterVersion is not PDF', () => { - const authoringTemplate = { - ...basePdfProofingTemplate, - letterVersion: 'AUTHORING', - } as unknown as AuthoringLetterTemplate; - - expect(() => assertPdfProofingLetter(authoringTemplate)).toThrow( - UnexpectedLetterVersionError - ); - expect(() => assertPdfProofingLetter(authoringTemplate)).toThrow( - 'ERR_UNEXPECTED_LETTER_VERSION: AUTHORING' - ); - }); - }); -}); diff --git a/utils/utils/src/email-client.ts b/utils/utils/src/email-client.ts index f10f4f603..9cc63e5d6 100644 --- a/utils/utils/src/email-client.ts +++ b/utils/utils/src/email-client.ts @@ -3,10 +3,7 @@ import { createMimeMessage } from 'mimetext'; import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'; import { TemplateDto } from 'nhs-notify-backend-client'; import { Logger } from 'nhs-notify-web-template-management-utils/logger'; -import { - LetterTemplate, - assertPdfProofingLetter, -} from 'nhs-notify-web-template-management-utils'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; import Handlebars from 'handlebars'; import { readFileSync } from 'node:fs'; import path from 'node:path'; @@ -185,8 +182,11 @@ export class EmailClient { return; } - // nothing to send if template is not a letter - if (template.templateType !== 'LETTER') { + // nothing to send if template is not a PDF letter + if ( + template.templateType !== 'LETTER' || + template.letterVersion !== 'PDF' + ) { this.logger.info({ description: 'Not sending template submitted email to suppliers because templateType is not LETTER', @@ -196,11 +196,10 @@ export class EmailClient { return; } - const pdfProofingLetter = assertPdfProofingLetter(template); const proofsBySupplier: Record = {}; for (const { supplier, fileName } of Object.values( - pdfProofingLetter.files.proofs ?? {} + template.files.proofs ?? {} )) { const proofFilenames = [...(proofsBySupplier[supplier] ?? []), fileName]; proofsBySupplier[supplier] = proofFilenames; @@ -208,7 +207,7 @@ export class EmailClient { for (const [supplier, proofFilenames] of Object.entries(proofsBySupplier)) { await this.sendTemplateSubmittedEmailToSupplier( - pdfProofingLetter, + template, supplier, proofFilenames ); diff --git a/utils/utils/src/types.ts b/utils/utils/src/types.ts index 1d869cb11..c4581f998 100644 --- a/utils/utils/src/types.ts +++ b/utils/utils/src/types.ts @@ -67,22 +67,6 @@ export type LetterTemplate = | PdfProofingLetterTemplate | AuthoringLetterTemplate; -export class UnexpectedLetterVersionError extends Error { - constructor(letterVersion: string) { - super(`ERR_UNEXPECTED_LETTER_VERSION: ${letterVersion}`); - this.name = 'UnexpectedLetterVersionError'; - } -} - -export const assertPdfProofingLetter = ( - template: LetterTemplate -): PdfProofingLetterTemplate => { - if (template.letterVersion !== 'PDF') { - throw new UnexpectedLetterVersionError(template.letterVersion); - } - return template; -}; - export type TemplateFormState = FormState & T; From 03b1b483881ef6cdaad816e01d82821bc24d5bf4 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 09:06:21 +0000 Subject: [PATCH 08/74] accept new letter types in rc template selection --- .../ChooseChannelTemplate/server-action.ts | 4 +-- .../server-action.ts | 15 ++++------ frontend/src/utils/routing-utils.ts | 28 ++++++++----------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts index 2a4bd371d..498134f99 100644 --- a/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts +++ b/frontend/src/components/forms/ChooseChannelTemplate/server-action.ts @@ -5,7 +5,6 @@ import { updateRoutingConfig } from '@utils/message-plans'; import { ChooseChannelTemplateProps } from './choose-channel-template.types'; import { isLetterTemplate, - isPdfProofingLetter, addAccessibleFormatLetterTemplateToCascade, addDefaultTemplateToCascade, } from '@utils/routing-utils'; @@ -56,8 +55,7 @@ export async function chooseChannelTemplateAction( const updatedCascade = isAccessibleFormatTemplate && selectedTemplate && - isLetterTemplate(selectedTemplate) && - isPdfProofingLetter(selectedTemplate) + isLetterTemplate(selectedTemplate) ? addAccessibleFormatLetterTemplateToCascade( messagePlan.cascade, cascadeIndex, diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts index d4eb06089..46712f106 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts @@ -1,14 +1,11 @@ import { redirect, RedirectType } from 'next/navigation'; import { FormState, - PdfProofingLetterTemplate, + LetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { z } from 'zod'; import { updateRoutingConfig } from '@utils/message-plans'; -import { - replaceLanguageTemplatesInCascadeItem, - isPdfProofingLetter, -} from '@utils/routing-utils'; +import { replaceLanguageTemplatesInCascadeItem } from '@utils/routing-utils'; import { Language, $LockNumber } from 'nhs-notify-backend-client'; import { ChooseLanguageLetterTemplatesProps } from './ChooseLanguageLetterTemplates'; import baseContent from '@content/content'; @@ -91,12 +88,10 @@ export async function chooseLanguageLetterTemplatesAction( } const selectedTemplateIdsSet = new Set(selectedTemplateIds); - const templateMap = new Map( + const templateMap = new Map( templateList - .filter( - (template): template is PdfProofingLetterTemplate => - selectedTemplateIdsSet.has(template.id) && - isPdfProofingLetter(template) + .filter((template): template is LetterTemplate => + selectedTemplateIdsSet.has(template.id) ) .map((template) => [template.id, template]) ); diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts index 258df6581..40ab393fa 100644 --- a/frontend/src/utils/routing-utils.ts +++ b/frontend/src/utils/routing-utils.ts @@ -27,15 +27,6 @@ export function isLetterTemplate( return template.templateType === 'LETTER'; } -/** - * Type guard to check if a letter template is a PDF proofing letter - */ -export function isPdfProofingLetter( - template: LetterTemplate -): template is PdfProofingLetterTemplate { - return template.letterVersion === 'PDF'; -} - /** * Gets the conditional templates for a cascade item, from the provided templates object */ @@ -205,8 +196,7 @@ export function addDefaultTemplateToCascade( defaultTemplateId: selectedTemplateId, ...(selectedTemplate && isLetterTemplate(selectedTemplate) && - isPdfProofingLetter(selectedTemplate) && - selectedTemplate.supplierReferences && { + 'supplierReferences' in selectedTemplate && { supplierReferences: selectedTemplate.supplierReferences, }), }; @@ -219,12 +209,14 @@ export function addDefaultTemplateToCascade( */ export function addAccessibleFormatLetterTemplateToCascadeItem( cascadeItem: CascadeItem, - selectedTemplate: PdfProofingLetterTemplate + selectedTemplate: LetterTemplate ): CascadeItem { const newConditionalTemplate: ConditionalTemplateAccessible = { accessibleFormat: selectedTemplate.letterType, templateId: selectedTemplate.id, - supplierReferences: selectedTemplate.supplierReferences, + ...('supplierReferences' in selectedTemplate && { + supplierReferences: selectedTemplate.supplierReferences, + }), }; const conditionalTemplates = [...(cascadeItem.conditionalTemplates ?? [])]; @@ -253,7 +245,7 @@ export function addAccessibleFormatLetterTemplateToCascadeItem( export function addAccessibleFormatLetterTemplateToCascade( cascade: CascadeItem[], cascadeIndex: number, - selectedTemplate: PdfProofingLetterTemplate + selectedTemplate: LetterTemplate ): CascadeItem[] { const updatedCascade = [...cascade]; updatedCascade[cascadeIndex] = addAccessibleFormatLetterTemplateToCascadeItem( @@ -268,7 +260,7 @@ export function addAccessibleFormatLetterTemplateToCascade( */ export function addLanguageLetterTemplatesToCascadeItem( cascadeItem: CascadeItem, - selectedTemplates: PdfProofingLetterTemplate[] + selectedTemplates: LetterTemplate[] ): CascadeItem { if (selectedTemplates.length === 0) { return cascadeItem; @@ -282,7 +274,9 @@ export function addLanguageLetterTemplatesToCascadeItem( return { language: template.language, templateId: template.id, - supplierReferences: template.supplierReferences, + ...('supplierReferences' in template && { + supplierReferences: template.supplierReferences, + }), }; }); @@ -343,7 +337,7 @@ export function removeLanguageTemplatesFromCascadeItem( */ export function replaceLanguageTemplatesInCascadeItem( cascadeItem: CascadeItem, - selectedTemplates: PdfProofingLetterTemplate[] + selectedTemplates: LetterTemplate[] ): CascadeItem { const cascadeItemWithoutLanguages = removeLanguageTemplatesFromCascadeItem(cascadeItem); From 3d543c077d034ee0faf707fb70b60567eff808d0 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 10:01:19 +0000 Subject: [PATCH 09/74] rename PdfProofing etc type to Pdf --- .../app/preview-letter-template/page.test.tsx | 4 +- .../app/submit-letter-template/page.test.tsx | 4 +- frontend/src/__tests__/helpers/helpers.ts | 6 +-- frontend/src/utils/routing-utils.ts | 4 +- .../modules/backend-api/spec.tmpl.json | 8 +-- .../src/__tests__/fixtures/template.ts | 8 +-- .../src/__tests__/schemas/template.test.ts | 14 +++--- .../backend-client/src/schemas/template.ts | 49 +++++++++---------- .../src/types/generated/index.ts | 4 +- .../src/types/generated/types.gen.ts | 8 +-- .../utils/src/__tests__/email-client.test.ts | 4 +- utils/utils/src/types.ts | 9 ++-- utils/utils/src/zod-validators.ts | 8 +-- 13 files changed, 62 insertions(+), 68 deletions(-) diff --git a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx index 221375e57..a98010c17 100644 --- a/frontend/src/__tests__/app/preview-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-letter-template/page.test.tsx @@ -6,7 +6,7 @@ import PreviewLetterTemplatePage, { } from '@app/preview-letter-template/[templateId]/page'; import { PreviewLetterTemplate } from '@organisms/PreviewLetterTemplate/PreviewLetterTemplate'; import { - type PdfProofingLetterTemplate, + type PdfLetterTemplate, type LetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { redirect } from 'next/navigation'; @@ -126,7 +126,7 @@ describe('PreviewLetterTemplatePage', () => { }, { description: 'a letter where files is the wrong data type', - files: [] as unknown as PdfProofingLetterTemplate['files'], + files: [] as unknown as PdfLetterTemplate['files'], }, ])( 'should redirect to invalid-template when template is $description', diff --git a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx index c3b93cfcf..5676a6a4a 100644 --- a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx @@ -14,7 +14,7 @@ import { NHS_APP_TEMPLATE, SMS_TEMPLATE, } from '@testhelpers/helpers'; -import { PdfProofingLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; import content from '@content/content'; import { serverIsFeatureEnabled } from '@utils/server-features'; @@ -74,7 +74,7 @@ describe('SubmitLetterTemplatePage', () => { SMS_TEMPLATE, { ...LETTER_TEMPLATE, - files: undefined as unknown as PdfProofingLetterTemplate['files'], + files: undefined as unknown as PdfLetterTemplate['files'], } as TemplateDto, { ...LETTER_TEMPLATE, diff --git a/frontend/src/__tests__/helpers/helpers.ts b/frontend/src/__tests__/helpers/helpers.ts index e72585fd4..c906feb0d 100644 --- a/frontend/src/__tests__/helpers/helpers.ts +++ b/frontend/src/__tests__/helpers/helpers.ts @@ -1,6 +1,6 @@ import { mockDeep } from 'jest-mock-extended'; import { RoutingConfig, TemplateDto } from 'nhs-notify-backend-client'; -import { PdfProofingLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; function* iteratorFromList(list: T[]): IterableIterator { for (const item of list) { @@ -52,7 +52,7 @@ export const SMS_TEMPLATE: TemplateDto = { lockNumber: 1, } as const; -export const LETTER_TEMPLATE: PdfProofingLetterTemplate = { +export const LETTER_TEMPLATE: PdfLetterTemplate = { id: 'letter-template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', @@ -72,7 +72,7 @@ export const LETTER_TEMPLATE: PdfProofingLetterTemplate = { lockNumber: 1, } as const; -export const LARGE_PRINT_LETTER_TEMPLATE: PdfProofingLetterTemplate = { +export const LARGE_PRINT_LETTER_TEMPLATE: PdfLetterTemplate = { id: 'large-print-letter-template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts index 40ab393fa..55ad43a05 100644 --- a/frontend/src/utils/routing-utils.ts +++ b/frontend/src/utils/routing-utils.ts @@ -9,7 +9,7 @@ import { } from 'nhs-notify-backend-client'; import { LetterTemplate, - PdfProofingLetterTemplate, + PdfLetterTemplate, } from 'nhs-notify-web-template-management-utils'; export type ConditionalTemplate = @@ -297,7 +297,7 @@ export function addLanguageLetterTemplatesToCascadeItem( export function addLanguageLetterTemplatesToCascade( cascade: CascadeItem[], cascadeIndex: number, - selectedTemplates: PdfProofingLetterTemplate[] + selectedTemplates: PdfLetterTemplate[] ): CascadeItem[] { const updatedCascade = [...cascade]; updatedCascade[cascadeIndex] = addLanguageLetterTemplatesToCascadeItem( diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index 275f054e1..ed33280ec 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -443,7 +443,7 @@ ], "type": "object" }, - "CreatePdfProofingLetterProperties": { + "CreatePdfLetterProperties": { "allOf": [ { "$ref": "#/components/schemas/BaseLetterTemplateProperties" @@ -506,7 +506,7 @@ "$ref": "#/components/schemas/NhsAppProperties" }, { - "$ref": "#/components/schemas/CreatePdfProofingLetterProperties" + "$ref": "#/components/schemas/CreatePdfLetterProperties" } ] } @@ -611,7 +611,7 @@ "$ref": "#/components/schemas/AuthoringLetterProperties" }, { - "$ref": "#/components/schemas/PdfProofingLetterProperties" + "$ref": "#/components/schemas/PdfLetterProperties" } ] }, @@ -648,7 +648,7 @@ ], "type": "object" }, - "PdfProofingLetterProperties": { + "PdfLetterProperties": { "allOf": [ { "$ref": "#/components/schemas/BaseLetterTemplateProperties" diff --git a/lambdas/backend-api/src/__tests__/fixtures/template.ts b/lambdas/backend-api/src/__tests__/fixtures/template.ts index 7f6ece439..d03beb0de 100644 --- a/lambdas/backend-api/src/__tests__/fixtures/template.ts +++ b/lambdas/backend-api/src/__tests__/fixtures/template.ts @@ -4,7 +4,7 @@ import type { NhsAppProperties, SmsProperties, TemplateDto, - CreatePdfProofingLetterProperties, + CreatePdfLetterProperties, } from 'nhs-notify-backend-client'; import { WithAttachments } from '../../infra/template-repository'; import { DatabaseTemplate } from 'nhs-notify-web-template-management-utils'; @@ -30,7 +30,7 @@ const nhsAppProperties: NhsAppProperties = { templateType: 'NHS_APP', }; -const letterProperties: WithAttachments = { +const letterProperties: WithAttachments = { templateType: 'LETTER', letterType: 'x0', language: 'en', @@ -135,9 +135,9 @@ export const makeSmsTemplate = ( export const makeLetterTemplate = ( overrides: Partial< - TemplateDto & WithAttachments + TemplateDto & WithAttachments > = {} -): TemplateFixture => { +): TemplateFixture => { const createUpdateTemplate = { ...createTemplateProperties, ...letterProperties, diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index 07a956b6d..e0dae90c5 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -1,10 +1,10 @@ import { $AuthoringLetterProperties, - $CreatePdfProofingLetterProperties, + $CreatePdfLetterProperties, $CreateUpdateNonLetter, $CreateUpdateTemplate, $LetterProperties, - $PdfProofingLetterProperties, + $PdfLetterProperties, $TemplateDto, $TemplateFilter, } from '../../schemas'; @@ -203,7 +203,7 @@ describe('Template schemas', () => { }); test('Letter template fields - should fail validation, when no letterType', async () => { - const result = $CreatePdfProofingLetterProperties.safeParse({ + const result = $CreatePdfLetterProperties.safeParse({ name: 'Test Template', campaignId: 'campaign-id', templateType: 'LETTER', @@ -255,7 +255,7 @@ describe('Template schemas', () => { }); }); - describe('$PdfProofingLetterProperties', () => { + describe('$PdfLetterProperties', () => { const validPdfProofingLetter = { templateType: 'LETTER', letterType: 'x0', @@ -271,16 +271,14 @@ describe('Template schemas', () => { }; test('should pass validation for valid PDF letter', () => { - const result = $PdfProofingLetterProperties.safeParse( - validPdfProofingLetter - ); + const result = $PdfLetterProperties.safeParse(validPdfProofingLetter); expect(result.success).toBe(true); expect(result.data).toEqual(validPdfProofingLetter); }); test('should fail validation when letterVersion is not PDF', () => { - const result = $PdfProofingLetterProperties.safeParse({ + const result = $PdfLetterProperties.safeParse({ ...validPdfProofingLetter, letterVersion: 'AUTHORING', }); diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index f64ab4477..f1a517d41 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -2,13 +2,13 @@ import { z } from 'zod/v4'; import { AuthoringLetterProperties, BaseTemplate, - CreatePdfProofingLetterProperties, + CreatePdfLetterProperties, CreateUpdateTemplate, EmailProperties, VersionedFileDetails, ProofFileDetails, LetterFiles, - PdfProofingLetterProperties, + PdfLetterProperties, NhsAppProperties, SmsProperties, LetterType, @@ -89,25 +89,24 @@ export const $BaseLetterTemplateProperties = z.object({ language: z.enum(LANGUAGE_LIST), }); -export const $CreatePdfProofingLetterProperties = - schemaFor()( +export const $CreatePdfLetterProperties = + schemaFor()( z.object({ ...$BaseLetterTemplateProperties.shape, campaignId: z.string(), }) ); -export const $PdfProofingLetterProperties = - schemaFor()( - z.object({ - ...$BaseLetterTemplateProperties.shape, - files: $LetterFiles, - letterVersion: z.literal('PDF'), - personalisationParameters: z.array(z.string()).optional(), - proofingEnabled: z.boolean().optional(), - supplierReferences: z.record(z.string(), z.string()).optional(), - }) - ); +export const $PdfLetterProperties = schemaFor()( + z.object({ + ...$BaseLetterTemplateProperties.shape, + files: $LetterFiles, + letterVersion: z.literal('PDF'), + personalisationParameters: z.array(z.string()).optional(), + proofingEnabled: z.boolean().optional(), + supplierReferences: z.record(z.string(), z.string()).optional(), + }) +); export const $AuthoringLetterProperties = schemaFor()( @@ -119,17 +118,17 @@ export const $AuthoringLetterProperties = }) ); -const $DefaultablePdfProofingLetterVersion = z +const $DefaultablePdfLetterVersion = z .union([z.undefined(), z.literal('PDF')]) .transform((val) => val ?? ('PDF' as const)); -const $PdfProofingLetterPropertiesWithDefault = z.object({ - ...$PdfProofingLetterProperties.shape, - letterVersion: $DefaultablePdfProofingLetterVersion, +const $PdfLetterPropertiesWithDefault = z.object({ + ...$PdfLetterProperties.shape, + letterVersion: $DefaultablePdfLetterVersion, }); export const $LetterProperties = z.discriminatedUnion('letterVersion', [ - $PdfProofingLetterPropertiesWithDefault, + $PdfLetterPropertiesWithDefault, $AuthoringLetterProperties, ]); @@ -155,7 +154,7 @@ export const $CreateUpdateTemplate = schemaFor()( $BaseTemplateSchema.extend($NhsAppProperties.shape), $BaseTemplateSchema.extend($EmailProperties.shape), $BaseTemplateSchema.extend($SmsProperties.shape), - $BaseTemplateSchema.extend($CreatePdfProofingLetterProperties.shape), + $BaseTemplateSchema.extend($CreatePdfLetterProperties.shape), ]) ); @@ -207,13 +206,13 @@ const $AuthoringLetterTemplateDto = $BaseTemplateDto.extend( $AuthoringLetterProperties.shape ); -const $PdfProofingLetterTemplateDto = $BaseTemplateDto.extend({ - ...$PdfProofingLetterProperties.shape, - letterVersion: $DefaultablePdfProofingLetterVersion, +const $PdfLetterTemplateDto = $BaseTemplateDto.extend({ + ...$PdfLetterProperties.shape, + letterVersion: $DefaultablePdfLetterVersion, }); const $LetterTemplateDto = z.discriminatedUnion('letterVersion', [ - $PdfProofingLetterTemplateDto, + $PdfLetterTemplateDto, $AuthoringLetterTemplateDto, ]); diff --git a/lambdas/backend-client/src/types/generated/index.ts b/lambdas/backend-client/src/types/generated/index.ts index 6ae023ac6..822a18481 100644 --- a/lambdas/backend-client/src/types/generated/index.ts +++ b/lambdas/backend-client/src/types/generated/index.ts @@ -22,7 +22,7 @@ export type { ConditionalTemplateAccessible, ConditionalTemplateLanguage, CountSuccess, - CreatePdfProofingLetterProperties, + CreatePdfLetterProperties, CreateRoutingConfig, CreateUpdateTemplate, DeleteV1RoutingConfigurationByRoutingConfigIdData, @@ -93,7 +93,7 @@ export type { PatchV1TemplateByTemplateIdSubmitErrors, PatchV1TemplateByTemplateIdSubmitResponse, PatchV1TemplateByTemplateIdSubmitResponses, - PdfProofingLetterProperties, + PdfLetterProperties, PostV1LetterTemplateData, PostV1LetterTemplateError, PostV1LetterTemplateErrors, diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index d44345c65..fed4379dd 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -125,7 +125,7 @@ export type CountSuccess = { statusCode: number; }; -export type CreatePdfProofingLetterProperties = BaseLetterTemplateProperties & { +export type CreatePdfLetterProperties = BaseLetterTemplateProperties & { campaignId: string; }; @@ -141,7 +141,7 @@ export type CreateUpdateTemplate = BaseTemplate & | SmsProperties | EmailProperties | NhsAppProperties - | CreatePdfProofingLetterProperties + | CreatePdfLetterProperties ); export type EmailProperties = { @@ -197,7 +197,7 @@ export type LetterFiles = { export type LetterProperties = | AuthoringLetterProperties - | PdfProofingLetterProperties; + | PdfLetterProperties; export type LetterType = 'q4' | 'x0' | 'x1'; @@ -208,7 +208,7 @@ export type NhsAppProperties = { templateType: 'NHS_APP'; }; -export type PdfProofingLetterProperties = BaseLetterTemplateProperties & { +export type PdfLetterProperties = BaseLetterTemplateProperties & { files: LetterFiles; letterVersion: 'PDF'; personalisationParameters?: Array; diff --git a/utils/utils/src/__tests__/email-client.test.ts b/utils/utils/src/__tests__/email-client.test.ts index 025b46e52..bc220cac3 100644 --- a/utils/utils/src/__tests__/email-client.test.ts +++ b/utils/utils/src/__tests__/email-client.test.ts @@ -3,7 +3,7 @@ import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'; import { Logger } from 'nhs-notify-web-template-management-utils/logger'; import { EmailClient } from '../email-client'; import { TemplateDto } from 'nhs-notify-backend-client'; -import { PdfProofingLetterTemplate } from '../types'; +import { PdfLetterTemplate } from '../types'; describe('EmailClient', () => { const recipientEmails = { @@ -73,7 +73,7 @@ describe('EmailClient', () => { }); describe('template-submitted email', () => { - const mockTemplate = mockDeep({ + const mockTemplate = mockDeep({ id: 'template-id', templateType: 'LETTER', letterVersion: 'PDF', diff --git a/utils/utils/src/types.ts b/utils/utils/src/types.ts index c4581f998..873e6c350 100644 --- a/utils/utils/src/types.ts +++ b/utils/utils/src/types.ts @@ -7,7 +7,7 @@ import { LetterFiles, LetterType, LetterVersion, - PdfProofingLetterProperties, + PdfLetterProperties, TemplateDto, TemplateStatus, TemplateType, @@ -57,15 +57,12 @@ export type EmailTemplate = Extract; export type SMSTemplate = Extract; -export type PdfProofingLetterTemplate = BaseCreatedTemplate & - PdfProofingLetterProperties; +export type PdfLetterTemplate = BaseCreatedTemplate & PdfLetterProperties; export type AuthoringLetterTemplate = BaseCreatedTemplate & AuthoringLetterProperties; -export type LetterTemplate = - | PdfProofingLetterTemplate - | AuthoringLetterTemplate; +export type LetterTemplate = PdfLetterTemplate | AuthoringLetterTemplate; export type TemplateFormState = FormState & T; diff --git a/utils/utils/src/zod-validators.ts b/utils/utils/src/zod-validators.ts index f32954558..0db0a6598 100644 --- a/utils/utils/src/zod-validators.ts +++ b/utils/utils/src/zod-validators.ts @@ -1,11 +1,11 @@ import { z } from 'zod'; import { - $CreatePdfProofingLetterProperties, + $CreatePdfLetterProperties, $CreateUpdateTemplate, $EmailProperties, $Language, $LetterFiles, - $PdfProofingLetterProperties, + $PdfLetterProperties, $LetterType, $NhsAppProperties, $SmsProperties, @@ -74,11 +74,11 @@ export const $SubmittedSMSTemplate = z.intersection( export const $UploadLetterTemplate = z.intersection( $CreateUpdateTemplate, - $CreatePdfProofingLetterProperties + $CreatePdfLetterProperties ); export const $LetterTemplate = z.intersection( $TemplateDto, - $PdfProofingLetterProperties.extend({ files: $LetterFiles }) + $PdfLetterProperties.extend({ files: $LetterFiles }) ); export const $SubmittedLetterTemplate = z.intersection( $SubmittedTemplate, From 7d67efb8d6d4c805e2f9f1c94e7b0637446e451d Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 10:06:57 +0000 Subject: [PATCH 10/74] rm useless type guard --- .../forms/ChooseLanguageLetterTemplates/server-action.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts index 46712f106..fa0794849 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/server-action.ts @@ -90,9 +90,7 @@ export async function chooseLanguageLetterTemplatesAction( const selectedTemplateIdsSet = new Set(selectedTemplateIds); const templateMap = new Map( templateList - .filter((template): template is LetterTemplate => - selectedTemplateIdsSet.has(template.id) - ) + .filter((template) => selectedTemplateIdsSet.has(template.id)) .map((template) => [template.id, template]) ); From b858152efc3b427e2bcb3c99c2c1521b15430eb0 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 10:26:19 +0000 Subject: [PATCH 11/74] require version on API requests --- .../__snapshots__/page.test.tsx.snap | 2 ++ .../LetterTemplateForm/server-action.test.ts | 1 + .../src/__tests__/utils/form-actions.test.ts | 4 +++ .../src/app/upload-letter-template/page.tsx | 1 + .../modules/backend-api/spec.tmpl.json | 9 ++++++- .../src/__tests__/api/upload-letter.test.ts | 1 + .../src/__tests__/app/template-client.test.ts | 27 ++++++++++--------- .../src/__tests__/fixtures/template.ts | 1 + .../infra/get-letter-upload-parts.test.ts | 1 + .../infra/template-repository/repository.ts | 6 +++-- .../src/__tests__/schemas/template.test.ts | 5 ++++ .../src/__tests__/template-api-client.test.ts | 2 ++ .../backend-client/src/schemas/template.ts | 1 + .../src/types/generated/types.gen.ts | 5 ++-- 14 files changed, 47 insertions(+), 19 deletions(-) diff --git a/frontend/src/__tests__/app/upload-letter-template/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/upload-letter-template/__snapshots__/page.test.tsx.snap index 1a7d7bdc3..eedb450fd 100644 --- a/frontend/src/__tests__/app/upload-letter-template/__snapshots__/page.test.tsx.snap +++ b/frontend/src/__tests__/app/upload-letter-template/__snapshots__/page.test.tsx.snap @@ -12,6 +12,7 @@ exports[`UploadLetterTemplatePage should render UploadLetterTemplatePage when on "campaignId": "campaign-id", "language": "en", "letterType": "x0", + "letterVersion": "PDF", "name": "", "templateType": "LETTER", } @@ -32,6 +33,7 @@ exports[`UploadLetterTemplatePage should render UploadLetterTemplatePage with mu "campaignId": "", "language": "en", "letterType": "x0", + "letterVersion": "PDF", "name": "", "templateType": "LETTER", } diff --git a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts index b54f20b2d..18922f1f6 100644 --- a/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts +++ b/frontend/src/__tests__/components/forms/LetterTemplateForm/server-action.test.ts @@ -17,6 +17,7 @@ const initialState: UploadLetterTemplate = { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; describe('UploadLetterTemplate server actions', () => { diff --git a/frontend/src/__tests__/utils/form-actions.test.ts b/frontend/src/__tests__/utils/form-actions.test.ts index feb944185..1d76e9854 100644 --- a/frontend/src/__tests__/utils/form-actions.test.ts +++ b/frontend/src/__tests__/utils/form-actions.test.ts @@ -156,6 +156,7 @@ describe('form-actions', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['file contents'], 'template.pdf', { @@ -212,6 +213,7 @@ describe('form-actions', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['file contents'], 'template.pdf', { @@ -253,6 +255,7 @@ describe('form-actions', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['file contents'], 'template.pdf', { @@ -283,6 +286,7 @@ describe('form-actions', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['file contents'], 'template.pdf', { diff --git a/frontend/src/app/upload-letter-template/page.tsx b/frontend/src/app/upload-letter-template/page.tsx index 0e97fc547..580b9a231 100644 --- a/frontend/src/app/upload-letter-template/page.tsx +++ b/frontend/src/app/upload-letter-template/page.tsx @@ -32,6 +32,7 @@ const UploadLetterTemplatePage = async () => { letterType: 'x0', language: 'en', campaignId: campaignIds.length === 1 ? campaignIds[0] : '', + letterVersion: 'PDF', }; return ( diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index ed33280ec..a36598b08 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -452,10 +452,17 @@ "properties": { "campaignId": { "type": "string" + }, + "letterVersion": { + "enum": [ + "PDF" + ], + "type": "string" } }, "required": [ - "campaignId" + "campaignId", + "letterVersion" ] } ] diff --git a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts index 75ea84e3f..aab5751f0 100644 --- a/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts +++ b/lambdas/backend-api/src/__tests__/api/upload-letter.test.ts @@ -33,6 +33,7 @@ describe('upload-letter', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = Buffer.from('letterPdf'); diff --git a/lambdas/backend-api/src/__tests__/app/template-client.test.ts b/lambdas/backend-api/src/__tests__/app/template-client.test.ts index 5d6b3eac1..0dc1aaf22 100644 --- a/lambdas/backend-api/src/__tests__/app/template-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/template-client.test.ts @@ -171,6 +171,7 @@ describe('templateClient', () => { letterType: 'x0', language: 'en', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const result = await templateClient.createTemplate(data, user); @@ -349,7 +350,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -491,7 +492,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -591,7 +592,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: undefined, - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', } as unknown as CreateUpdateTemplate; const pdf = new File(['pdf'], 'template.pdf', { @@ -671,7 +672,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const result = await templateClient.uploadLetterTemplate( @@ -718,7 +719,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -753,7 +754,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -783,7 +784,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -818,7 +819,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -883,7 +884,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -924,7 +925,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -1022,7 +1023,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -1122,7 +1123,7 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -1283,7 +1284,7 @@ describe('templateClient', () => { templateType: 'LETTER', language: 'it', letterType: 'x1', - campaignId: 'campaign-id', + campaignId: 'campaign-id', letterVersion: 'PDF', }; const result = await templateClient.updateTemplate( diff --git a/lambdas/backend-api/src/__tests__/fixtures/template.ts b/lambdas/backend-api/src/__tests__/fixtures/template.ts index d03beb0de..75ac4a477 100644 --- a/lambdas/backend-api/src/__tests__/fixtures/template.ts +++ b/lambdas/backend-api/src/__tests__/fixtures/template.ts @@ -34,6 +34,7 @@ const letterProperties: WithAttachments = { templateType: 'LETTER', letterType: 'x0', language: 'en', + letterVersion: 'PDF', files: { pdfTemplate: { fileName: 'template.pdf', diff --git a/lambdas/backend-api/src/__tests__/infra/get-letter-upload-parts.test.ts b/lambdas/backend-api/src/__tests__/infra/get-letter-upload-parts.test.ts index 185c55757..23f5b38f1 100644 --- a/lambdas/backend-api/src/__tests__/infra/get-letter-upload-parts.test.ts +++ b/lambdas/backend-api/src/__tests__/infra/get-letter-upload-parts.test.ts @@ -9,6 +9,7 @@ describe('getLetterUploadParts', () => { letterType: 'x0', language: 'en', campaignId: 'camapign-id', + letterVersion: 'PDF', }; const pdf = Buffer.from('letterPdf'); diff --git a/lambdas/backend-api/src/infra/template-repository/repository.ts b/lambdas/backend-api/src/infra/template-repository/repository.ts index 3f2ce7475..3a1c2af8c 100644 --- a/lambdas/backend-api/src/infra/template-repository/repository.ts +++ b/lambdas/backend-api/src/infra/template-repository/repository.ts @@ -29,7 +29,10 @@ import { calculateTTL } from '@backend-api/utils/calculate-ttl'; import { ApplicationResult, failure, success } from '../../utils'; import { TemplateQuery } from './query'; -export type WithAttachments = T extends { templateType: 'LETTER' } +export type WithAttachments = T extends { + templateType: 'LETTER'; + letterVersion: 'PDF'; +} ? T & { files: LetterFiles } : T; @@ -89,7 +92,6 @@ export class TemplateRepository { createdBy: this.internalUserKey(user), ...(template.templateType === 'LETTER' && { campaignId, - letterVersion: 'PDF' as const, }), lockNumber: 0, }; diff --git a/lambdas/backend-client/src/__tests__/schemas/template.test.ts b/lambdas/backend-client/src/__tests__/schemas/template.test.ts index e0dae90c5..da49c49e5 100644 --- a/lambdas/backend-client/src/__tests__/schemas/template.test.ts +++ b/lambdas/backend-client/src/__tests__/schemas/template.test.ts @@ -88,6 +88,7 @@ describe('Template schemas', () => { campaignId: 'campaign-id', letterType: 'x0', language: 'en', + letterVersion: 'PDF', }, }, ])( @@ -144,6 +145,7 @@ describe('Template schemas', () => { campaignId: 'campaign-id', letterType: 'x0', language: 'en', + letterVersion: 'PDF', }, }, ])( @@ -173,6 +175,7 @@ describe('Template schemas', () => { language: 'ar', name: 'letter', campaignId: 'campaign-id', + letterVersion: 'PDF', }; const result = $CreateUpdateNonLetter.safeParse(letter); @@ -208,6 +211,7 @@ describe('Template schemas', () => { campaignId: 'campaign-id', templateType: 'LETTER', language: 'en', + letterVersion: 'PDF', }); expect(result.error?.flatten()).toEqual( @@ -247,6 +251,7 @@ describe('Template schemas', () => { templateType: 'LETTER', letterType: 'x0', language: 'en', + letterVersion: 'PDF', }, ])('should pass validation %p', async (template) => { const result = $CreateUpdateTemplate.safeParse(template); diff --git a/lambdas/backend-client/src/__tests__/template-api-client.test.ts b/lambdas/backend-client/src/__tests__/template-api-client.test.ts index 2794737b4..5782f7650 100644 --- a/lambdas/backend-client/src/__tests__/template-api-client.test.ts +++ b/lambdas/backend-client/src/__tests__/template-api-client.test.ts @@ -92,6 +92,7 @@ describe('TemplateAPIClient', () => { language: 'de', letterType: 'x1', campaignId: 'campaign-id', + letterVersion: 'PDF', }, testToken, new File(['pdf'], 'template.pdf', { type: 'application/pdf' }) @@ -143,6 +144,7 @@ describe('TemplateAPIClient', () => { language: 'de', letterType: 'x1', campaignId: 'campaign-id', + letterVersion: 'PDF', }, testToken, new File(['pdf'], 'template.pdf', { type: 'application/pdf' }), diff --git a/lambdas/backend-client/src/schemas/template.ts b/lambdas/backend-client/src/schemas/template.ts index f1a517d41..143567ebd 100644 --- a/lambdas/backend-client/src/schemas/template.ts +++ b/lambdas/backend-client/src/schemas/template.ts @@ -94,6 +94,7 @@ export const $CreatePdfLetterProperties = z.object({ ...$BaseLetterTemplateProperties.shape, campaignId: z.string(), + letterVersion: z.literal('PDF'), }) ); diff --git a/lambdas/backend-client/src/types/generated/types.gen.ts b/lambdas/backend-client/src/types/generated/types.gen.ts index fed4379dd..32bfac23b 100644 --- a/lambdas/backend-client/src/types/generated/types.gen.ts +++ b/lambdas/backend-client/src/types/generated/types.gen.ts @@ -127,6 +127,7 @@ export type CountSuccess = { export type CreatePdfLetterProperties = BaseLetterTemplateProperties & { campaignId: string; + letterVersion: 'PDF'; }; export type CreateRoutingConfig = { @@ -195,9 +196,7 @@ export type LetterFiles = { testDataCsv?: VersionedFileDetails; }; -export type LetterProperties = - | AuthoringLetterProperties - | PdfLetterProperties; +export type LetterProperties = AuthoringLetterProperties | PdfLetterProperties; export type LetterType = 'q4' | 'x0' | 'x1'; From 019939985a8786994c1ef7706936fb373a175411 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 10:35:50 +0000 Subject: [PATCH 12/74] add version field to automated test input --- .../test-team/helpers/factories/template-api-payload-factory.ts | 1 + tests/test-team/helpers/factories/template-factory.ts | 1 + tests/test-team/helpers/types.ts | 1 + .../template-mgmt-api-tests/update-template.api.spec.ts | 1 + .../letter/template-mgmt-preview-letter-page.component.spec.ts | 1 + .../letter/template-mgmt-submit-letter-page.component.spec.ts | 1 + 6 files changed, 6 insertions(+) diff --git a/tests/test-team/helpers/factories/template-api-payload-factory.ts b/tests/test-team/helpers/factories/template-api-payload-factory.ts index d9b792bbf..ca15f3199 100644 --- a/tests/test-team/helpers/factories/template-api-payload-factory.ts +++ b/tests/test-team/helpers/factories/template-api-payload-factory.ts @@ -37,6 +37,7 @@ const createTemplateBaseData = (templateType: unknown) => ({ ...(templateType === 'LETTER' && { language: 'en', letterType: 'x0', + letterVersion: 'PDF', }), }); diff --git a/tests/test-team/helpers/factories/template-factory.ts b/tests/test-team/helpers/factories/template-factory.ts index 0db40197b..d6af4aecb 100644 --- a/tests/test-team/helpers/factories/template-factory.ts +++ b/tests/test-team/helpers/factories/template-factory.ts @@ -86,6 +86,7 @@ export const TemplateFactory = { id, language: options?.language || 'en', letterType: options?.letterType || 'x0', + letterVersion: 'PDF', name, owner: `CLIENT#${user.clientId}`, templateStatus, diff --git a/tests/test-team/helpers/types.ts b/tests/test-team/helpers/types.ts index 11d6eb0ce..90722c4bb 100644 --- a/tests/test-team/helpers/types.ts +++ b/tests/test-team/helpers/types.ts @@ -47,6 +47,7 @@ type TypeSpecificProperties = { message?: string; subject?: string; letterType?: string; + letterVersion?: string; language?: string; files?: { pdfTemplate?: File; diff --git a/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts b/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts index 7fa975146..49b4904c9 100644 --- a/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts @@ -245,6 +245,7 @@ test.describe('PUT /v1/template/:templateId', () => { campaignId: 'Campaign1', language: 'en', letterType: 'x0', + letterVersion: 'PDF', templateStatus: 'SUBMITTED', }), } diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts index f55160fab..54b7c2e0a 100644 --- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-preview-letter-page.component.spec.ts @@ -49,6 +49,7 @@ async function createTemplates() { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', owner: `CLIENT#${user.clientId}`, + letterVersion: 'PDF', } as Template, notYetSubmitted: TemplateFactory.uploadLetterTemplate( '9AACCD57-C6A3-4273-854C-3839A081B4D9', diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-submit-letter-page.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-submit-letter-page.component.spec.ts index 11cc2e2e0..f30a23afb 100644 --- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-submit-letter-page.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-submit-letter-page.component.spec.ts @@ -43,6 +43,7 @@ async function createTemplates() { templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', owner: `CLIENT#${routingEnabledUser.clientId}`, + letterVersion: 'PDF', } as Template, routingEnabled: TemplateFactory.uploadLetterTemplate( From 6e463be846a4551206652be48c44b4d4bbd67320 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 10:58:45 +0000 Subject: [PATCH 13/74] fmt fix --- .../src/__tests__/app/template-client.test.ts | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lambdas/backend-api/src/__tests__/app/template-client.test.ts b/lambdas/backend-api/src/__tests__/app/template-client.test.ts index 0dc1aaf22..a460379d9 100644 --- a/lambdas/backend-api/src/__tests__/app/template-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/template-client.test.ts @@ -350,7 +350,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -492,7 +493,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -592,7 +594,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: undefined, - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', } as unknown as CreateUpdateTemplate; const pdf = new File(['pdf'], 'template.pdf', { @@ -672,7 +675,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const result = await templateClient.uploadLetterTemplate( @@ -719,7 +723,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -754,7 +759,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -784,7 +790,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; mocks.clientConfigRepository.get.mockResolvedValueOnce({ @@ -819,7 +826,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -884,7 +892,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -925,7 +934,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -1023,7 +1033,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], 'template.pdf', { @@ -1123,7 +1134,8 @@ describe('templateClient', () => { name: 'name', language: 'en', letterType: 'x0', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const pdf = new File(['pdf'], pdfFilename, { @@ -1284,7 +1296,8 @@ describe('templateClient', () => { templateType: 'LETTER', language: 'it', letterType: 'x1', - campaignId: 'campaign-id', letterVersion: 'PDF', + campaignId: 'campaign-id', + letterVersion: 'PDF', }; const result = await templateClient.updateTemplate( From e35f6587043db9c0b0938c3a89c873471c3d4869 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 11:22:15 +0000 Subject: [PATCH 14/74] typecheck fix post merge --- frontend/src/__tests__/utils/render-template-markdown.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/__tests__/utils/render-template-markdown.test.ts b/frontend/src/__tests__/utils/render-template-markdown.test.ts index 1e2989adf..0ad771681 100644 --- a/frontend/src/__tests__/utils/render-template-markdown.test.ts +++ b/frontend/src/__tests__/utils/render-template-markdown.test.ts @@ -72,6 +72,7 @@ test('throws error for unsupported template type', () => { id: '', lockNumber: 0, name: '', + letterVersion: 'PDF', templateStatus: 'NOT_YET_SUBMITTED', templateType: 'LETTER', updatedAt: '', From 0cb52c40a3bac63878667753e028c25a044c6db5 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 11:22:56 +0000 Subject: [PATCH 15/74] backend readme --- lambdas/backend-api/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambdas/backend-api/README.md b/lambdas/backend-api/README.md index ed081b107..e9f6d12cb 100644 --- a/lambdas/backend-api/README.md +++ b/lambdas/backend-api/README.md @@ -97,7 +97,8 @@ curl -X POST --location "${APIG_STAGE}/v1/letter-template" \ "templateType": "LETTER", "name": "example letter", "letterType": "x0", - "language": "en" + "language": "en", + "letterVersion": "PDF" }' ``` From 8ad2880a0bceb7db7e9fb331b5900636784aaa19 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 11:58:55 +0000 Subject: [PATCH 16/74] fix API test assertions --- .../src/__tests__/app/template-client.test.ts | 40 +++++++++++++++++++ .../upload-letter-template.api.spec.ts | 2 + 2 files changed, 42 insertions(+) diff --git a/lambdas/backend-api/src/__tests__/app/template-client.test.ts b/lambdas/backend-api/src/__tests__/app/template-client.test.ts index a460379d9..39809eb1e 100644 --- a/lambdas/backend-api/src/__tests__/app/template-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/template-client.test.ts @@ -17,6 +17,7 @@ import { isoDateRegExp } from 'nhs-notify-web-template-management-test-helper-ut import { ClientConfigRepository } from '../../infra/client-config-repository'; import { RoutingConfigRepository } from '../../infra/routing-config-repository'; import { isRightToLeft } from 'nhs-notify-web-template-management-utils/enum'; + import { TemplateQuery } from '../../infra/template-repository/query'; import { TemplateFilter } from 'nhs-notify-backend-client/src/types/filters'; @@ -650,6 +651,45 @@ describe('templateClient', () => { expect(mocks.templateRepository.create).not.toHaveBeenCalled(); }); + // temporary, until we implement support for AUTHORING letter version + test('should return a failure result, when letterVersion is not PDF', async () => { + const { templateClient, mocks } = setup(); + + const data = { + templateType: 'LETTER', + name: 'name', + language: 'en', + letterType: 'x0', + campaignId: 'campaign-id', + letterVersion: 'AUTHORING', + }; + + const pdf = new File(['pdf'], 'template.pdf', { + type: 'application/pdf', + }); + + const result = await templateClient.uploadLetterTemplate( + data as CreateUpdateTemplate, + user, + pdf + ); + + expect(result).toEqual({ + error: expect.objectContaining({ + errorMeta: expect.objectContaining({ + code: 400, + description: 'Request failed validation', + }), + }), + }); + + expect(result.error?.errorMeta.details).toMatchObject({ + letterVersion: expect.stringContaining('Invalid input: expected "PDF"'), + }); + + expect(mocks.templateRepository.create).not.toHaveBeenCalled(); + }); + const invalidPdfCases: { case: string; pdf: File }[] = [ { case: 'no file type', diff --git a/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts b/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts index dd3637129..00d1b5d8a 100644 --- a/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts +++ b/tests/test-team/template-mgmt-api-tests/upload-letter-template.api.spec.ts @@ -98,6 +98,7 @@ test.describe('POST /v1/letter-template', () => { id: expect.stringMatching(uuidRegExp), language: 'en', letterType: 'x0', + letterVersion: 'PDF', name: templateData.name, proofingEnabled: true, templateStatus: 'PENDING_VALIDATION', @@ -181,6 +182,7 @@ test.describe('POST /v1/letter-template', () => { id: expect.stringMatching(uuidRegExp), language: 'en', letterType: 'x0', + letterVersion: 'PDF', name: templateData.name, proofingEnabled: true, templateStatus: 'PENDING_VALIDATION', From 118bd4609886d51df7fc7db8569dc0d1001a16b8 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 12:05:36 +0000 Subject: [PATCH 17/74] improve routing utils handling of letters --- .../components/molecules/PreviewTemplateDetails.test.tsx | 1 + frontend/src/utils/routing-utils.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index 3d3075902..5be09f2ed 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -69,6 +69,7 @@ describe('PreviewTemplateDetailsSms', () => { }); describe('PreviewTemplateDetailsLetter', () => { + // temporary, until we implement support for AUTHORING letter version it('throws error for AUTHORING letter version', () => { expect(() => render( diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts index 01f97f2bb..a3efb13ac 100644 --- a/frontend/src/utils/routing-utils.ts +++ b/frontend/src/utils/routing-utils.ts @@ -208,7 +208,7 @@ export function addDefaultTemplateToCascade( defaultTemplateId: selectedTemplateId, ...(selectedTemplate && isLetterTemplate(selectedTemplate) && - 'supplierReferences' in selectedTemplate && { + selectedTemplate.letterVersion === 'PDF' && { supplierReferences: selectedTemplate.supplierReferences, }), }; @@ -226,7 +226,7 @@ export function addAccessibleFormatLetterTemplateToCascadeItem( const newConditionalTemplate: ConditionalTemplateAccessible = { accessibleFormat: selectedTemplate.letterType, templateId: selectedTemplate.id, - ...('supplierReferences' in selectedTemplate && { + ...(selectedTemplate.letterVersion === 'PDF' && { supplierReferences: selectedTemplate.supplierReferences, }), }; @@ -286,7 +286,7 @@ export function addLanguageLetterTemplatesToCascadeItem( return { language: template.language, templateId: template.id, - ...('supplierReferences' in template && { + ...(template.letterVersion === 'PDF' && { supplierReferences: template.supplierReferences, }), }; From ce1206d2092e560a8dac1d98ebd367d9988f8d0a Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 12:59:19 +0000 Subject: [PATCH 18/74] use pdf types on the frontend --- .../__tests__/app/copy-template/page.test.tsx | 4 ++-- .../page.test.tsx | 4 ++-- .../ChooseLanguageLetterTemplates.test.tsx | 8 +++---- .../LanguageLetterTemplates.test.tsx | 8 +++---- .../molecules/PreviewTemplateDetails.test.tsx | 24 ------------------- .../LanguageLetterTemplates.tsx | 4 ++-- .../PreviewTemplateDetailsLetter.tsx | 8 ++----- .../PreviewLetterTemplate.tsx | 4 ++-- 8 files changed, 18 insertions(+), 46 deletions(-) diff --git a/frontend/src/__tests__/app/copy-template/page.test.tsx b/frontend/src/__tests__/app/copy-template/page.test.tsx index 1a4b4c8f0..bac4eddad 100644 --- a/frontend/src/__tests__/app/copy-template/page.test.tsx +++ b/frontend/src/__tests__/app/copy-template/page.test.tsx @@ -6,7 +6,7 @@ import { CopyTemplate } from '@forms/CopyTemplate/CopyTemplate'; import { redirect } from 'next/navigation'; import { getTemplate } from '@utils/form-actions'; import { TemplateDto } from 'nhs-notify-backend-client'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; jest.mock('@utils/form-actions'); jest.mock('next/navigation'); @@ -30,7 +30,7 @@ describe('CopyTemplatePage', () => { lockNumber: 1, } satisfies TemplateDto; - const letterTemplate: LetterTemplate = { + const letterTemplate: PdfLetterTemplate = { id: 'template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', diff --git a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx index 010973e29..ae3286f48 100644 --- a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx @@ -4,7 +4,7 @@ import PreviewSubmittedLetterTemplatePage, { generateMetadata, } from '@app/preview-submitted-letter-template/[templateId]/page'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; import { getTemplate } from '@utils/form-actions'; import { redirect } from 'next/navigation'; import { TemplateDto } from 'nhs-notify-backend-client'; @@ -50,7 +50,7 @@ describe('PreviewSubmittedLetterTemplatePage', () => { lockNumber: 1, } satisfies TemplateDto; - const submittedLetterTemplate: LetterTemplate = { + const submittedLetterTemplate: PdfLetterTemplate = { ...templateDTO, templateStatus: 'SUBMITTED', }; diff --git a/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx b/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx index c67da626e..ea7d1c175 100644 --- a/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx +++ b/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { LETTER_TEMPLATE, ROUTING_CONFIG } from '@testhelpers/helpers'; import { useActionState } from 'react'; import { ChooseLanguageLetterTemplatesFormState } from '@forms/ChooseLanguageLetterTemplates/server-action'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; import { ConditionalTemplateLanguage } from 'nhs-notify-backend-client'; jest.mock('react', () => { @@ -25,21 +25,21 @@ jest.mock('react', () => { }; }); -const FRENCH_LETTER_TEMPLATE: LetterTemplate = { +const FRENCH_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'french-letter-id', name: 'French letter template', language: 'fr', }; -const POLISH_LETTER_TEMPLATE: LetterTemplate = { +const POLISH_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'polish-letter-id', name: 'Polish letter template', language: 'pl', }; -const SPANISH_LETTER_TEMPLATE: LetterTemplate = { +const SPANISH_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'spanish-letter-id', name: 'Spanish letter template', diff --git a/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx b/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx index 55781a724..35cf5ce90 100644 --- a/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx +++ b/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx @@ -2,7 +2,7 @@ import { LanguageLetterTemplates } from '@molecules/LanguageLetterTemplates/Lang import { LETTER_TEMPLATE } from '@testhelpers/helpers'; import { render } from '@testing-library/react'; import { usePathname } from 'next/navigation'; -import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; jest.mock('next/navigation'); @@ -12,21 +12,21 @@ jest 'message-plans/choose-other-language-letter-template/testid' ); -const FRENCH_LETTER_TEMPLATE: LetterTemplate = { +const FRENCH_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'french-template-id', name: 'French Letter Template', language: 'fr', }; -const POLISH_LETTER_TEMPLATE: LetterTemplate = { +const POLISH_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'polish-template-id', name: 'Polish Letter Template', language: 'pl', }; -const GERMAN_LETTER_TEMPLATE: LetterTemplate = { +const GERMAN_LETTER_TEMPLATE: PdfLetterTemplate = { ...LETTER_TEMPLATE, id: 'german-template-id', name: 'German Letter Template', diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index 5be09f2ed..3508ed5fd 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -69,30 +69,6 @@ describe('PreviewTemplateDetailsSms', () => { }); describe('PreviewTemplateDetailsLetter', () => { - // temporary, until we implement support for AUTHORING letter version - it('throws error for AUTHORING letter version', () => { - expect(() => - render( - - ) - ).toThrow('AUTHORING letter version is not implemented'); - }); - it('matches snapshot without proofs', () => { const container = render( ) { +}: Readonly<{ template: PdfLetterTemplate }>) { const { approveProofText, backLinkText, From 14e89b15ce1ce980fb404bcb172cff36dea5aac0 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 13:16:02 +0000 Subject: [PATCH 19/74] filter to PDF templates --- .../[routingConfigId]/page.tsx | 6 +++++- .../ChooseLanguageLetterTemplates.tsx | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx b/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx index edfdc1045..345454bf1 100644 --- a/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx +++ b/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx @@ -41,6 +41,10 @@ export default async function ChooseOtherLanguageLetterTemplate( }), ]); + const pdfLetterTemplates = foreignLanguageTemplates.filter( + (template) => template.letterVersion === 'PDF' + ); + if (!messagePlan) { return redirect('/message-plans/invalid', RedirectType.replace); } @@ -57,7 +61,7 @@ export default async function ChooseOtherLanguageLetterTemplate( diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx index cd3382258..d6163aa65 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx @@ -7,7 +7,7 @@ import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton'; import { useActionState, useState } from 'react'; import { ErrorState, - LetterTemplate, + PdfLetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary'; import { @@ -28,7 +28,7 @@ const content = baseContent.components.chooseLanguageLetterTemplates; export type ChooseLanguageLetterTemplatesProps = { messagePlan: RoutingConfig; pageHeading: string; - templateList: LetterTemplate[]; + templateList: PdfLetterTemplate[]; cascadeIndex: number; lockNumber: number; }; From aa7e761d50c6294139e55bc591f1099fef40d48a Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 14:16:19 +0000 Subject: [PATCH 20/74] allow fetching AUTHORING letters on the frontend --- .../__tests__/app/copy-template/page.test.tsx | 4 ++-- .../page.test.tsx | 4 ++-- .../ChooseLanguageLetterTemplates.test.tsx | 8 +++---- .../LanguageLetterTemplates.test.tsx | 8 +++---- .../molecules/PreviewTemplateDetails.test.tsx | 24 +++++++++++++++++++ .../LanguageLetterTemplates.tsx | 4 ++-- .../PreviewTemplateDetailsLetter.tsx | 8 +++++-- .../PreviewLetterTemplate.tsx | 8 +++++-- .../backend-api/src/app/template-client.ts | 4 ++-- utils/utils/src/zod-validators.ts | 24 +++++++++++++++++-- 10 files changed, 74 insertions(+), 22 deletions(-) diff --git a/frontend/src/__tests__/app/copy-template/page.test.tsx b/frontend/src/__tests__/app/copy-template/page.test.tsx index bac4eddad..1a4b4c8f0 100644 --- a/frontend/src/__tests__/app/copy-template/page.test.tsx +++ b/frontend/src/__tests__/app/copy-template/page.test.tsx @@ -6,7 +6,7 @@ import { CopyTemplate } from '@forms/CopyTemplate/CopyTemplate'; import { redirect } from 'next/navigation'; import { getTemplate } from '@utils/form-actions'; import { TemplateDto } from 'nhs-notify-backend-client'; -import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; jest.mock('@utils/form-actions'); jest.mock('next/navigation'); @@ -30,7 +30,7 @@ describe('CopyTemplatePage', () => { lockNumber: 1, } satisfies TemplateDto; - const letterTemplate: PdfLetterTemplate = { + const letterTemplate: LetterTemplate = { id: 'template-id', templateType: 'LETTER', templateStatus: 'NOT_YET_SUBMITTED', diff --git a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx index ae3286f48..010973e29 100644 --- a/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/preview-submitted-letter-template/page.test.tsx @@ -4,7 +4,7 @@ import PreviewSubmittedLetterTemplatePage, { generateMetadata, } from '@app/preview-submitted-letter-template/[templateId]/page'; -import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; import { getTemplate } from '@utils/form-actions'; import { redirect } from 'next/navigation'; import { TemplateDto } from 'nhs-notify-backend-client'; @@ -50,7 +50,7 @@ describe('PreviewSubmittedLetterTemplatePage', () => { lockNumber: 1, } satisfies TemplateDto; - const submittedLetterTemplate: PdfLetterTemplate = { + const submittedLetterTemplate: LetterTemplate = { ...templateDTO, templateStatus: 'SUBMITTED', }; diff --git a/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx b/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx index ea7d1c175..c67da626e 100644 --- a/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx +++ b/frontend/src/__tests__/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { LETTER_TEMPLATE, ROUTING_CONFIG } from '@testhelpers/helpers'; import { useActionState } from 'react'; import { ChooseLanguageLetterTemplatesFormState } from '@forms/ChooseLanguageLetterTemplates/server-action'; -import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; import { ConditionalTemplateLanguage } from 'nhs-notify-backend-client'; jest.mock('react', () => { @@ -25,21 +25,21 @@ jest.mock('react', () => { }; }); -const FRENCH_LETTER_TEMPLATE: PdfLetterTemplate = { +const FRENCH_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'french-letter-id', name: 'French letter template', language: 'fr', }; -const POLISH_LETTER_TEMPLATE: PdfLetterTemplate = { +const POLISH_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'polish-letter-id', name: 'Polish letter template', language: 'pl', }; -const SPANISH_LETTER_TEMPLATE: PdfLetterTemplate = { +const SPANISH_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'spanish-letter-id', name: 'Spanish letter template', diff --git a/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx b/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx index 35cf5ce90..55781a724 100644 --- a/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx +++ b/frontend/src/__tests__/components/molecules/LanguageLetterTemplates.test.tsx @@ -2,7 +2,7 @@ import { LanguageLetterTemplates } from '@molecules/LanguageLetterTemplates/Lang import { LETTER_TEMPLATE } from '@testhelpers/helpers'; import { render } from '@testing-library/react'; import { usePathname } from 'next/navigation'; -import { PdfLetterTemplate } from 'nhs-notify-web-template-management-utils'; +import { LetterTemplate } from 'nhs-notify-web-template-management-utils'; jest.mock('next/navigation'); @@ -12,21 +12,21 @@ jest 'message-plans/choose-other-language-letter-template/testid' ); -const FRENCH_LETTER_TEMPLATE: PdfLetterTemplate = { +const FRENCH_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'french-template-id', name: 'French Letter Template', language: 'fr', }; -const POLISH_LETTER_TEMPLATE: PdfLetterTemplate = { +const POLISH_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'polish-template-id', name: 'Polish Letter Template', language: 'pl', }; -const GERMAN_LETTER_TEMPLATE: PdfLetterTemplate = { +const GERMAN_LETTER_TEMPLATE: LetterTemplate = { ...LETTER_TEMPLATE, id: 'german-template-id', name: 'German Letter Template', diff --git a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx index 3508ed5fd..5be09f2ed 100644 --- a/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx +++ b/frontend/src/__tests__/components/molecules/PreviewTemplateDetails.test.tsx @@ -69,6 +69,30 @@ describe('PreviewTemplateDetailsSms', () => { }); describe('PreviewTemplateDetailsLetter', () => { + // temporary, until we implement support for AUTHORING letter version + it('throws error for AUTHORING letter version', () => { + expect(() => + render( + + ) + ).toThrow('AUTHORING letter version is not implemented'); + }); + it('matches snapshot without proofs', () => { const container = render( ) { +}: Readonly<{ template: LetterTemplate }>) { + if (template.letterVersion !== 'PDF') { + throw new Error('AUTHORING letter version is not supported'); + } + const { approveProofText, backLinkText, diff --git a/lambdas/backend-api/src/app/template-client.ts b/lambdas/backend-api/src/app/template-client.ts index 7a483cc0b..b06fdd2ed 100644 --- a/lambdas/backend-api/src/app/template-client.ts +++ b/lambdas/backend-api/src/app/template-client.ts @@ -18,7 +18,7 @@ import { $UploadLetterTemplate, DatabaseTemplate, User, - $LetterTemplate, + $PdfLetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { isRightToLeft } from 'nhs-notify-web-template-management-utils/enum'; import { Logger } from 'nhs-notify-web-template-management-utils/logger'; @@ -32,7 +32,7 @@ import { RoutingConfigRepository } from '@backend-api/infra/routing-config-repos export class TemplateClient { private $LetterForProofing = z.intersection( - $LetterTemplate, + $PdfLetterTemplate, z.object({ templateType: z.literal('LETTER'), personalisationParameters: z.array(z.string()), diff --git a/utils/utils/src/zod-validators.ts b/utils/utils/src/zod-validators.ts index 0db0a6598..573ccb13a 100644 --- a/utils/utils/src/zod-validators.ts +++ b/utils/utils/src/zod-validators.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { + $AuthoringLetterProperties, $CreatePdfLetterProperties, $CreateUpdateTemplate, $EmailProperties, @@ -76,10 +77,29 @@ export const $UploadLetterTemplate = z.intersection( $CreateUpdateTemplate, $CreatePdfLetterProperties ); -export const $LetterTemplate = z.intersection( - $TemplateDto, + +// Base template fields shared by all letter templates +const $BaseLetterTemplateDto = $TemplateDto.and( + z.object({ + templateType: z.literal('LETTER'), + }) +); + +export const $PdfLetterTemplate = $BaseLetterTemplateDto.and( $PdfLetterProperties.extend({ files: $LetterFiles }) ); + +export const $AuthoringLetterTemplate = $BaseLetterTemplateDto.and( + $AuthoringLetterProperties +); + +// Union of both letter template types - use z.union since discriminatedUnion +// requires object schemas (not intersections) with the discriminator at top level +export const $LetterTemplate = z.union([ + $PdfLetterTemplate, + $AuthoringLetterTemplate, +]); + export const $SubmittedLetterTemplate = z.intersection( $SubmittedTemplate, $LetterTemplate From 29b555800b1516844065cdb5afa98ab95c0cfed9 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 14:17:38 +0000 Subject: [PATCH 21/74] rm comment --- utils/utils/src/zod-validators.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/utils/src/zod-validators.ts b/utils/utils/src/zod-validators.ts index 573ccb13a..7d892ea56 100644 --- a/utils/utils/src/zod-validators.ts +++ b/utils/utils/src/zod-validators.ts @@ -93,8 +93,6 @@ export const $AuthoringLetterTemplate = $BaseLetterTemplateDto.and( $AuthoringLetterProperties ); -// Union of both letter template types - use z.union since discriminatedUnion -// requires object schemas (not intersections) with the discriminator at top level export const $LetterTemplate = z.union([ $PdfLetterTemplate, $AuthoringLetterTemplate, From 0e1c87d75b3c0ae0c3dd2d42cb553acf1b5054ce Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 14:21:01 +0000 Subject: [PATCH 22/74] rm comment --- utils/utils/src/zod-validators.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/utils/src/zod-validators.ts b/utils/utils/src/zod-validators.ts index 7d892ea56..2b8fc926f 100644 --- a/utils/utils/src/zod-validators.ts +++ b/utils/utils/src/zod-validators.ts @@ -78,7 +78,6 @@ export const $UploadLetterTemplate = z.intersection( $CreatePdfLetterProperties ); -// Base template fields shared by all letter templates const $BaseLetterTemplateDto = $TemplateDto.and( z.object({ templateType: z.literal('LETTER'), From 17743559b9a2ddabd6ec66cdc265727722647a28 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 14:36:16 +0000 Subject: [PATCH 23/74] rm filter when chossing template --- .../[routingConfigId]/page.tsx | 6 +----- .../ChooseLanguageLetterTemplates.tsx | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx b/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx index 345454bf1..edfdc1045 100644 --- a/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx +++ b/frontend/src/app/message-plans/choose-other-language-letter-template/[routingConfigId]/page.tsx @@ -41,10 +41,6 @@ export default async function ChooseOtherLanguageLetterTemplate( }), ]); - const pdfLetterTemplates = foreignLanguageTemplates.filter( - (template) => template.letterVersion === 'PDF' - ); - if (!messagePlan) { return redirect('/message-plans/invalid', RedirectType.replace); } @@ -61,7 +57,7 @@ export default async function ChooseOtherLanguageLetterTemplate( diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx index d6163aa65..7e65fab2e 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx @@ -7,6 +7,7 @@ import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton'; import { useActionState, useState } from 'react'; import { ErrorState, + LetterTemplate, PdfLetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary'; @@ -28,7 +29,7 @@ const content = baseContent.components.chooseLanguageLetterTemplates; export type ChooseLanguageLetterTemplatesProps = { messagePlan: RoutingConfig; pageHeading: string; - templateList: PdfLetterTemplate[]; + templateList: LetterTemplate[]; cascadeIndex: number; lockNumber: number; }; From 4f0c4f2f8a6a05e12db1a6c1e2d13f35feb03fc7 Mon Sep 17 00:00:00 2001 From: "alex.nuttall1" Date: Fri, 30 Jan 2026 14:58:24 +0000 Subject: [PATCH 24/74] block some letter specific pages --- .../request-proof-of-template/page.test.tsx | 7 +++++- .../app/submit-letter-template/page.test.tsx | 4 ++- .../organisms/PreviewLetterTemplate.test.tsx | 23 +++++++++++++++++ frontend/src/__tests__/helpers/helpers.ts | 20 ++++++++++++++- .../[templateId]/page.tsx | 6 ++++- .../[templateId]/page.tsx | 2 +- .../ChooseLanguageLetterTemplates.tsx | 1 - .../helpers/factories/template-factory.ts | 25 +++++++++++++++++++ ...-mgmt-request-proof-page.component.spec.ts | 18 +++++++++++++ ...-mgmt-submit-letter-page.component.spec.ts | 22 ++++++++++++++++ 10 files changed, 122 insertions(+), 6 deletions(-) diff --git a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx index 8cbc5bcb0..6f29f5236 100644 --- a/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx +++ b/frontend/src/__tests__/app/request-proof-of-template/page.test.tsx @@ -9,6 +9,7 @@ import { redirect } from 'next/navigation'; import { getTemplate } from '@utils/form-actions'; import { TemplateDto } from 'nhs-notify-backend-client'; import { + AUTHORING_LETTER_TEMPLATE, EMAIL_TEMPLATE, LETTER_TEMPLATE, NHS_APP_TEMPLATE, @@ -104,8 +105,12 @@ describe('RequestProofPage', () => { ...LETTER_TEMPLATE, name: undefined as unknown as string, }, + { + ...AUTHORING_LETTER_TEMPLATE, + proofingEnabled: true, + }, ])( - 'should redirect to invalid-template when template is $templateType and name is $name', + 'should redirect to invalid-template when template is $templateType, letterVersion is $letterVersion and name is $name', async (value) => { getTemplateMock.mockResolvedValueOnce(value); diff --git a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx index 5676a6a4a..f9d3e18bf 100644 --- a/frontend/src/__tests__/app/submit-letter-template/page.test.tsx +++ b/frontend/src/__tests__/app/submit-letter-template/page.test.tsx @@ -9,6 +9,7 @@ import { redirect } from 'next/navigation'; import { getTemplate } from '@utils/form-actions'; import { TemplateDto } from 'nhs-notify-backend-client'; import { + AUTHORING_LETTER_TEMPLATE, EMAIL_TEMPLATE, LETTER_TEMPLATE, NHS_APP_TEMPLATE, @@ -86,8 +87,9 @@ describe('SubmitLetterTemplatePage', () => { }, }, } as TemplateDto, + AUTHORING_LETTER_TEMPLATE, ])( - 'should redirect to invalid-template when template is $templateType and LETTER required fields are missing', + 'should redirect to invalid-template when template is $templateType, letterVersion is $letterVersion and LETTER required fields are missing', async (value) => { getTemplateMock.mockResolvedValueOnce(value); diff --git a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx index c09fb4c9c..79d0a40ae 100644 --- a/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx +++ b/frontend/src/__tests__/components/organisms/PreviewLetterTemplate.test.tsx @@ -10,6 +10,29 @@ describe('PreviewLetterTemplate component', () => { jest.mocked(useFeatureFlags).mockReturnValue({ routing: false }); }); + it('throws error for AUTHORING letter version', () => { + expect(() => + render( + + ) + ).toThrow('AUTHORING letter version is not supported'); + }); + it('matches snapshot when template status is VIRUS_SCAN_FAILED', () => { const container = render( (list: T[]): IterableIterator { for (const item of list) { @@ -92,6 +95,21 @@ export const LARGE_PRINT_LETTER_TEMPLATE: PdfLetterTemplate = { lockNumber: 1, } as const; +export const AUTHORING_LETTER_TEMPLATE: AuthoringLetterTemplate = { + id: 'authoring-letter-template-id', + templateType: 'LETTER', + templateStatus: 'NOT_YET_SUBMITTED', + letterType: 'x0', + language: 'en', + letterVersion: 'AUTHORING', + letterVariantId: 'variant-123', + sidesCount: 2, + name: 'authoring letter template name', + createdAt: '2025-01-13T10:19:25.579Z', + updatedAt: '2025-01-13T10:19:25.579Z', + lockNumber: 1, +} as const; + export const ROUTING_CONFIG: RoutingConfig = { id: 'fbb81055-79b9-4759-ac07-d191ae57be34', name: 'Autumn Campaign Plan', diff --git a/frontend/src/app/request-proof-of-template/[templateId]/page.tsx b/frontend/src/app/request-proof-of-template/[templateId]/page.tsx index e9d3350a1..b542a5aa8 100644 --- a/frontend/src/app/request-proof-of-template/[templateId]/page.tsx +++ b/frontend/src/app/request-proof-of-template/[templateId]/page.tsx @@ -44,7 +44,11 @@ const RequestProofPage = async (props: TemplatePageProps) => { const validatedTemplate = validateLetterTemplate(template); - if (!validatedTemplate || !validatedTemplate.proofingEnabled) { + if ( + !validatedTemplate || + validatedTemplate.letterVersion !== 'PDF' || + !validatedTemplate.proofingEnabled + ) { return redirect('/invalid-template', RedirectType.replace); } diff --git a/frontend/src/app/submit-letter-template/[templateId]/page.tsx b/frontend/src/app/submit-letter-template/[templateId]/page.tsx index fece74e72..4aac6e074 100644 --- a/frontend/src/app/submit-letter-template/[templateId]/page.tsx +++ b/frontend/src/app/submit-letter-template/[templateId]/page.tsx @@ -42,7 +42,7 @@ const SubmitLetterTemplatePage = async (props: TemplatePageProps) => { const validatedTemplate = validateLetterTemplate(template); - if (!validatedTemplate) { + if (!validatedTemplate || validatedTemplate.letterVersion !== 'PDF') { return redirect('/invalid-template', RedirectType.replace); } diff --git a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx index 7e65fab2e..cd3382258 100644 --- a/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx +++ b/frontend/src/components/forms/ChooseLanguageLetterTemplates/ChooseLanguageLetterTemplates.tsx @@ -8,7 +8,6 @@ import { useActionState, useState } from 'react'; import { ErrorState, LetterTemplate, - PdfLetterTemplate, } from 'nhs-notify-web-template-management-utils'; import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary'; import { diff --git a/tests/test-team/helpers/factories/template-factory.ts b/tests/test-team/helpers/factories/template-factory.ts index c80250b29..e98dd2c3f 100644 --- a/tests/test-team/helpers/factories/template-factory.ts +++ b/tests/test-team/helpers/factories/template-factory.ts @@ -99,6 +99,31 @@ export const TemplateFactory = { }); }, + createAuthoringLetterTemplate: ( + id: string, + user: TestUser, + name: string, + templateStatus = 'NOT_YET_SUBMITTED', + options?: { + letterType?: LetterType; + language?: Language; + } + ): Template => { + return TemplateFactory.create({ + campaignId: 'campaign-id', + clientId: user.clientId, + id, + language: options?.language || 'en', + letterType: options?.letterType || 'x0', + letterVersion: 'AUTHORING', + name, + owner: `CLIENT#${user.clientId}`, + templateStatus, + templateType: 'LETTER', + proofingEnabled: true, + }); + }, + create: ( template: Partial