diff --git a/.github/workflows/stage-3-build.yaml b/.github/workflows/stage-3-build.yaml index a8441e7ff..766b9bfdd 100644 --- a/.github/workflows/stage-3-build.yaml +++ b/.github/workflows/stage-3-build.yaml @@ -161,7 +161,8 @@ jobs: --targetAccountGroup "nhs-notify-supplier-api-dev" \ --terraformAction "apply" \ --overrideProjectName "nhs" \ - --overrideRoleName "nhs-main-acct-supplier-api-github-deploy" + --overrideRoleName "nhs-main-acct-supplier-api-github-deploy" \ + --internalRef "feature/CCM-15148" artefact-proxies: name: "Build proxies" runs-on: ubuntu-latest diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index f1f278c5e..0e6b2dbec 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -34,7 +34,7 @@ No requirements. | [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes | | [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no | | [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no | -| [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string }))` |
{
"lv1": {
"specId": "spec1",
"supplierId": "supplier1"
},
"lv2": {
"specId": "spec2",
"supplierId": "supplier1"
},
"lv3": {
"specId": "spec3",
"supplierId": "supplier2"
}
} | no |
+| [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string, billingId = string }))` | {
"lv1": {
"billingId": "billing1",
"specId": "spec1",
"supplierId": "supplier1"
},
"lv2": {
"billingId": "billing2",
"specId": "spec2",
"supplierId": "supplier1"
},
"lv3": {
"billingId": "billing3",
"specId": "spec3",
"supplierId": "supplier2"
}
} | no |
| [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"INFO"` | no |
| [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no |
| [manually\_configure\_mtls\_truststore](#input\_manually\_configure\_mtls\_truststore) | Manually manage the truststore used for API Gateway mTLS (e.g. for prod environment) | `bool` | `false` | no |
diff --git a/infrastructure/terraform/components/api/variables.tf b/infrastructure/terraform/components/api/variables.tf
index 97169278e..d7faa9a81 100644
--- a/infrastructure/terraform/components/api/variables.tf
+++ b/infrastructure/terraform/components/api/variables.tf
@@ -136,11 +136,11 @@ variable "eventpub_control_plane_bus_arn" {
}
variable "letter_variant_map" {
- type = map(object({ supplierId = string, specId = string }))
+ type = map(object({ supplierId = string, specId = string, billingId = string }))
default = {
- "lv1" = { supplierId = "supplier1", specId = "spec1" },
- "lv2" = { supplierId = "supplier1", specId = "spec2" },
- "lv3" = { supplierId = "supplier2", specId = "spec3" }
+ "lv1" = { supplierId = "supplier1", specId = "spec1", billingId = "billing1" },
+ "lv2" = { supplierId = "supplier1", specId = "spec2", billingId = "billing2" },
+ "lv3" = { supplierId = "supplier2", specId = "spec3", billingId = "billing3" }
}
}
diff --git a/internal/datastore/src/__test__/letter-repository.test.ts b/internal/datastore/src/__test__/letter-repository.test.ts
index 193c1c077..0f6149717 100644
--- a/internal/datastore/src/__test__/letter-repository.test.ts
+++ b/internal/datastore/src/__test__/letter-repository.test.ts
@@ -30,6 +30,7 @@ function createLetter(
source: "/data-plane/letter-rendering/pdf",
subject: `client/1/letter-request/${letterId}`,
billingRef: "specification1",
+ specificationBillingId: "billing1",
};
}
diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts
index bb0843f82..62a3f6113 100644
--- a/internal/datastore/src/types.ts
+++ b/internal/datastore/src/types.ts
@@ -53,6 +53,7 @@ export const LetterSchema = LetterSchemaBase.extend({
source: z.string(),
subject: z.string(),
billingRef: z.string(),
+ specificationBillingId: z.string(),
}).describe("Letter");
/**
diff --git a/internal/events/package.json b/internal/events/package.json
index d8b6626c9..cf2e4f0e4 100644
--- a/internal/events/package.json
+++ b/internal/events/package.json
@@ -37,5 +37,5 @@
"typecheck": "tsc --noEmit"
},
"types": "dist/index.d.ts",
- "version": "1.0.13"
+ "version": "1.0.14"
}
diff --git a/internal/events/src/domain/letter.ts b/internal/events/src/domain/letter.ts
index 67ed8bbe8..c5aa00597 100644
--- a/internal/events/src/domain/letter.ts
+++ b/internal/events/src/domain/letter.ts
@@ -83,6 +83,13 @@ The identifier will be included as the origin domain in the subject of any corre
examples: ["1y3q9v1zzzz"],
}),
+ specificationBillingId: z.string().meta({
+ title: "Specification Billing ID",
+ description:
+ "The billing ID from the letter specification which was used to produce a letter pack for this request.",
+ examples: ["1y3q9v1zzzz"],
+ }),
+
supplierId: z.string().meta({
title: "Supplier ID",
description: "Supplier ID allocated to the letter during creation.",
diff --git a/internal/events/src/events/__tests__/letter-mapper.test.ts b/internal/events/src/events/__tests__/letter-mapper.test.ts
index c870dc91b..26ea6d00b 100644
--- a/internal/events/src/events/__tests__/letter-mapper.test.ts
+++ b/internal/events/src/events/__tests__/letter-mapper.test.ts
@@ -16,6 +16,7 @@ describe("letter-mapper", () => {
updatedAt: "2025-11-24T15:55:18.000Z",
source: "letter-rendering/source/test",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
} as Letter;
const source = "/data-plane/supplier-api/nhs-supplier-api-dev/main/letters";
const event = mapLetterToCloudEvent(letter, source);
@@ -35,6 +36,7 @@ describe("letter-mapper", () => {
status: "PRINTED",
specificationId: "spec1",
billingRef: "spec1",
+ specificationBillingId: "billing123",
supplierId: "supplier1",
groupId: "group1",
reasonCode: "R02",
diff --git a/internal/events/src/events/__tests__/letter-status-change-events.test.ts b/internal/events/src/events/__tests__/letter-status-change-events.test.ts
index 482155458..6ccd40205 100644
--- a/internal/events/src/events/__tests__/letter-status-change-events.test.ts
+++ b/internal/events/src/events/__tests__/letter-status-change-events.test.ts
@@ -40,6 +40,7 @@ describe("LetterStatus event validations", () => {
billingRef: "1y3q9v1zzzz",
groupId: "client_template",
status,
+ specificationBillingId: "billing123",
}),
}),
);
diff --git a/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-invalid-major-version.json b/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-invalid-major-version.json
index 192ea5e2e..81d4ded6a 100644
--- a/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-invalid-major-version.json
+++ b/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-invalid-major-version.json
@@ -9,6 +9,7 @@
"source": "/data-plane/letter-rendering/prod/render-pdf",
"subject": "client/00f3b388-bbe9-41c9-9e76-052d37ee8988/letter-request/0o5Fs0EELR0fUjHjbCnEtdUwQe4_0o5Fs0EELR0fUjHjbCnEtdUwQe5"
},
+ "specificationBillingId": "billing123",
"specificationId": "1y3q9v1zzzz",
"status": "ACCEPTED",
"supplierId": "supplier1"
diff --git a/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-missing-fields.json b/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-missing-fields.json
index 54000422a..0f03dc39f 100644
--- a/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-missing-fields.json
+++ b/internal/events/src/events/__tests__/testData/letter.ACCEPTED-with-missing-fields.json
@@ -8,6 +8,7 @@
"event": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"source": "/data-plane/letter-rendering/prod/render-pdf"
},
+ "specificationBillingId": "billing123",
"specificationId": "1y3q9v1zzzz",
"status": "ACCEPTED",
"supplierId": "supplier1"
diff --git a/internal/events/src/events/__tests__/testData/letter.ACCEPTED.json b/internal/events/src/events/__tests__/testData/letter.ACCEPTED.json
index 7ffac10f3..8182cc1fe 100644
--- a/internal/events/src/events/__tests__/testData/letter.ACCEPTED.json
+++ b/internal/events/src/events/__tests__/testData/letter.ACCEPTED.json
@@ -9,6 +9,7 @@
"source": "/data-plane/letter-rendering/prod/render-pdf",
"subject": "client/00f3b388-bbe9-41c9-9e76-052d37ee8988/letter-request/0o5Fs0EELR0fUjHjbCnEtdUwQe4_0o5Fs0EELR0fUjHjbCnEtdUwQe5"
},
+ "specificationBillingId": "billing123",
"specificationId": "1y3q9v1zzzz",
"status": "ACCEPTED",
"supplierId": "supplier1"
diff --git a/internal/events/src/events/__tests__/testData/letter.FORWARDED.json b/internal/events/src/events/__tests__/testData/letter.FORWARDED.json
index 28c6111f5..fe53b766e 100644
--- a/internal/events/src/events/__tests__/testData/letter.FORWARDED.json
+++ b/internal/events/src/events/__tests__/testData/letter.FORWARDED.json
@@ -11,6 +11,7 @@
},
"reasonCode": "RNIB",
"reasonText": "RNIB",
+ "specificationBillingId": "billing123",
"specificationId": "1y3q9v1zzzz",
"status": "FORWARDED",
"supplierId": "supplier1"
diff --git a/internal/events/src/events/__tests__/testData/letter.RETURNED.json b/internal/events/src/events/__tests__/testData/letter.RETURNED.json
index 07b28154e..f35440e42 100644
--- a/internal/events/src/events/__tests__/testData/letter.RETURNED.json
+++ b/internal/events/src/events/__tests__/testData/letter.RETURNED.json
@@ -11,6 +11,7 @@
},
"reasonCode": "R07",
"reasonText": "No such address",
+ "specificationBillingId": "billing123",
"specificationId": "1y3q9v1zzzz",
"status": "RETURNED",
"supplierId": "supplier1"
diff --git a/internal/events/src/events/letter-mapper.ts b/internal/events/src/events/letter-mapper.ts
index 91f72988a..4b6781e17 100644
--- a/internal/events/src/events/letter-mapper.ts
+++ b/internal/events/src/events/letter-mapper.ts
@@ -25,6 +25,7 @@ export function mapLetterToCloudEvent(
status: letter.status,
specificationId: letter.specificationId,
billingRef: letter.billingRef,
+ specificationBillingId: letter.specificationBillingId,
supplierId: letter.supplierId,
groupId: letter.groupId,
reasonCode: letter.reasonCode,
diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts
index fa7f9f81e..d6440eee5 100644
--- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts
+++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts
@@ -28,6 +28,7 @@ describe("letter-mapper", () => {
ttl: 123,
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
const result: PatchLetterResponse = mapToPatchLetterResponse(letter);
@@ -64,6 +65,7 @@ describe("letter-mapper", () => {
reasonText: "Reason text",
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
const result: PatchLetterResponse = mapToPatchLetterResponse(letter);
@@ -100,6 +102,7 @@ describe("letter-mapper", () => {
ttl: 123,
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
const result: GetLetterResponse = mapToGetLetterResponse(letter);
@@ -136,6 +139,7 @@ describe("letter-mapper", () => {
reasonText: "Reason text",
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
const result: GetLetterResponse = mapToGetLetterResponse(letter);
@@ -174,6 +178,7 @@ describe("letter-mapper", () => {
reasonText: "Reason text",
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
const result: GetLettersResponse = mapToGetLettersResponse([
diff --git a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
index 08e103ab0..be69387e1 100644
--- a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
+++ b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
@@ -41,6 +41,7 @@ function makeLetter(id: string, status: Letter["status"]): Letter {
reasonText: "Reason text",
source: "/data-plane/letter-rendering/pdf",
subject: "letter-rendering/source/letter/letter-id",
+ specificationBillingId: "billing123",
};
}
diff --git a/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts b/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
index fd11de133..9a805f44e 100644
--- a/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
+++ b/lambdas/letter-updates-transformer/src/__tests__/letter-updates-transformer.test.ts
@@ -339,6 +339,7 @@ function generateLetter(status: LetterStatus, id?: string): Letter {
supplierStatus: `supplier1#${status}`,
supplierStatusSk: "2025-12-10T11:12:54Z#1",
ttl: 1_234_567_890,
+ specificationBillingId: "billing1",
};
}
diff --git a/lambdas/supplier-allocator/src/config/__tests__/deps.test.ts b/lambdas/supplier-allocator/src/config/__tests__/deps.test.ts
index 6069dc07d..7c2767f11 100644
--- a/lambdas/supplier-allocator/src/config/__tests__/deps.test.ts
+++ b/lambdas/supplier-allocator/src/config/__tests__/deps.test.ts
@@ -7,6 +7,7 @@ describe("createDependenciesContainer", () => {
lv1: {
supplierId: "supplier1",
specId: "spec1",
+ billingId: "billing1",
},
},
};
diff --git a/lambdas/supplier-allocator/src/config/__tests__/env.test.ts b/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
index 1d8e3a1fc..d54c2c4f6 100644
--- a/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
+++ b/lambdas/supplier-allocator/src/config/__tests__/env.test.ts
@@ -19,7 +19,8 @@ describe("lambdaEnv", () => {
process.env.VARIANT_MAP = `{
"lv1": {
"supplierId": "supplier1",
- "specId": "spec1"
+ "specId": "spec1",
+ "billingId": "billing1"
}
}`;
@@ -31,6 +32,7 @@ describe("lambdaEnv", () => {
lv1: {
supplierId: "supplier1",
specId: "spec1",
+ billingId: "billing1",
},
},
});
diff --git a/lambdas/supplier-allocator/src/config/env.ts b/lambdas/supplier-allocator/src/config/env.ts
index 5d924430f..a3ef6bf0d 100644
--- a/lambdas/supplier-allocator/src/config/env.ts
+++ b/lambdas/supplier-allocator/src/config/env.ts
@@ -5,6 +5,7 @@ const LetterVariantSchema = z.record(
z.object({
supplierId: z.string(),
specId: z.string(),
+ billingId: z.string(),
}),
);
export type LetterVariant = z.infer