Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
e372fc7
Change supplier header to app-supplier-id
francisco-videira-nhs Aug 18, 2025
766c990
Validate path parameter only
francisco-videira-nhs Aug 18, 2025
6ae8678
Add basic error handling
francisco-videira-nhs Aug 21, 2025
fa437fb
Change supplier id header to nhsd-supplier-id
francisco-videira-nhs Aug 21, 2025
4a10a01
Validate request body
francisco-videira-nhs Aug 21, 2025
8ffd5a2
Move supplier id header to config
francisco-videira-nhs Aug 21, 2025
94511c2
Add debug config and workspace
francisco-videira-nhs Aug 21, 2025
780ba20
Remove root hello world handler
francisco-videira-nhs Aug 21, 2025
ee2e9ed
Merge remote-tracking branch 'origin/main' into feature/CCM-11188
francisco-videira-nhs Aug 22, 2025
4053511
Fix local reference
francisco-videira-nhs Aug 22, 2025
393b4fd
Add all letter status types
francisco-videira-nhs Aug 22, 2025
df11a4e
Fix letter schema
francisco-videira-nhs Aug 22, 2025
3a0c8b5
Merge remote-tracking branch 'origin/main' into feature/CCM-11188
francisco-videira-nhs Aug 22, 2025
1464338
CCM-11602: Test
simonlabarere Aug 29, 2025
9c8e408
CCM-11602: letters schema
simonlabarere Aug 29, 2025
1527d2c
CCM-11602: letters schema
simonlabarere Aug 29, 2025
f691d50
CCM-11602: Give get letters access to GSI
simonlabarere Aug 29, 2025
2fd858d
CCM-11602: GET Letters endpoint
simonlabarere Aug 29, 2025
e941df9
CCM-11602: GET Letters endpoint
simonlabarere Aug 29, 2025
740a199
CCM-11602: GET Letters endpoint
simonlabarere Aug 29, 2025
c487606
CCM-11602: GET Letters endpoint
simonlabarere Aug 29, 2025
1e7c493
CCM-11602: GET Letters endpoint
simonlabarere Aug 29, 2025
6b2aa91
CCM-11602: GET Endpoint sandbox updates
simonlabarere Sep 2, 2025
214ef5f
CCM-11602: Get enpoint tests
simonlabarere Sep 4, 2025
f30b190
CCM-11602: Get enpoint safe header check
simonlabarere Sep 4, 2025
4f671c8
CCM-11602: Update sandbox
simonlabarere Sep 4, 2025
19c2838
CCM-11602: Update sandbox
simonlabarere Sep 4, 2025
78671d2
CCM-11602: Add logging
simonlabarere Sep 4, 2025
ef55e54
CCM-11602: Update sandbox
simonlabarere Sep 4, 2025
2a82516
CCM-11602: Rename size parameter to limit
simonlabarere Sep 5, 2025
4939ecc
CCM-11602: Add validation for limit parameter
simonlabarere Sep 8, 2025
9f6d455
CCM-11602: increase Trivy scan timeout
simonlabarere Sep 8, 2025
d7ac071
CCM-11602: Add groupId
simonlabarere Sep 8, 2025
b5c0026
Correct target URL for main (#134)
nhsd-david-wass Aug 29, 2025
193aef5
CCM-11942 Fixing cross repo workflows (#137)
aidenvaines-cgi Sep 1, 2025
b1f5969
CCM-11751: Use github release assests for shared modules call (#142)
sidnhs Sep 8, 2025
e66b8d8
CCM-11751: Fixing workflow trigger (#146)
sidnhs Sep 8, 2025
97b73ab
CCM-11580: Updates for OAS Spec V1 (#139)
m-houston Sep 10, 2025
0828b17
Update README.md (#140)
timireland Sep 10, 2025
5ef7bed
Merge branch 'main' into feature/CCM-11602_get_endpoint
simonlabarere Sep 10, 2025
2855e68
Merge remote-tracking branch 'origin/main' into feature/CCM-11188
francisco-videira-nhs Sep 12, 2025
5d6e542
Merge branch 'main' into feature/CCM-11602_get_endpoint
masl2 Sep 15, 2025
2f97221
Add timestamp value to supplierStatusSk
francisco-videira-nhs Sep 15, 2025
efcbeb3
Merge branch 'feature/CCM-11602_get_endpoint' into feature/CCM-11188
francisco-videira-nhs Sep 15, 2025
e7be472
Bring get letters in line with patch letter
francisco-videira-nhs Sep 16, 2025
8382a2a
CCM-11602: param validation, max range, and OAS 400 examples
masl2 Sep 17, 2025
3b893c6
CCM-11602: unit test expectation wording
masl2 Sep 17, 2025
57ae2e2
Workspace change
francisco-videira-nhs Sep 17, 2025
5b14c24
Add reasonCode and reasonText to db
francisco-videira-nhs Sep 17, 2025
ddaf849
Log unexpected errors
francisco-videira-nhs Sep 17, 2025
99749c3
CCM-11602: MAX LIMIT envar and added into test context
masl2 Sep 17, 2025
2cfdedf
Remove reasonCode and text from plan
francisco-videira-nhs Sep 17, 2025
b8e4ef2
CCM-11602: limit can't be 0, return maxLimit value
masl2 Sep 17, 2025
bd9d919
Bump
francisco-videira-nhs Sep 18, 2025
534433d
Fix error
francisco-videira-nhs Sep 18, 2025
1a20406
Fix error message
francisco-videira-nhs Sep 18, 2025
651d163
Change sk to iso timestamp
francisco-videira-nhs Sep 18, 2025
829b599
Merge remote-tracking branch 'origin/feature/CCM-11602_get_endpoint' …
francisco-videira-nhs Sep 19, 2025
31c1fe3
Integrate get and patch branches
francisco-videira-nhs Sep 19, 2025
3bd8ac7
Merge remote-tracking branch 'origin/main' into feature/CCM-11188
francisco-videira-nhs Sep 19, 2025
7a57d8b
Workspace settings
francisco-videira-nhs Sep 19, 2025
6001e30
Add templates to error messages
francisco-videira-nhs Sep 19, 2025
3ffc1ce
Fix 400 limit messages
francisco-videira-nhs Sep 22, 2025
ded48aa
WIP Remove id from Error?
francisco-videira-nhs Sep 22, 2025
ff77676
CCM-11188: standardise shared modules
masl2 Sep 23, 2025
5fa01fa
Revert "WIP Remove id from Error?"
francisco-videira-nhs Sep 23, 2025
2761b90
Add correlationId as error.id
francisco-videira-nhs Sep 25, 2025
d0ae003
Marry errors with OAS
francisco-videira-nhs Sep 25, 2025
51675ac
Optional reason data
francisco-videira-nhs Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"configurations": [
{
"args": [
"${relativeFile}",
"--runInBand"
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/${input:workspace}",
"internalConsoleOptions": "neverOpen",
"name": "Debug current test file (pick workspace)",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"request": "launch",
"type": "node"
},
{
"args": [
"${relativeFile}",
"--runInBand",
"--testNamePattern",
"${input:testName}"
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/${input:workspace}",
"internalConsoleOptions": "neverOpen",
"name": "Debug single test by name (pick workspace)",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"request": "launch",
"type": "node"
}
],
"inputs": [
{
"description": "Select package workspace to run tests in",
"id": "workspace",
"options": [
"lambdas/api-handler",
"lambdas/authorizer",
"internal/datastore"
],
"type": "pickString"
},
{
"description": "Enter test name regex for --testNamePattern",
"id": "testName",
"type": "promptString"
}
],
"version": "0.0.1"
}
6 changes: 3 additions & 3 deletions infrastructure/terraform/components/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ No requirements.
| Name | Source | Version |
|------|--------|---------|
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
| <a name="module_domain_truststore"></a> [domain\_truststore](#module\_domain\_truststore) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/s3bucket | v2.0.17 |
| <a name="module_domain_truststore"></a> [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
| <a name="module_get_letters"></a> [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-kms.zip | n/a |
| <a name="module_logging_bucket"></a> [logging\_bucket](#module\_logging\_bucket) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/s3bucket | v2.0.17 |
| <a name="module_logging_bucket"></a> [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
| <a name="module_patch_letters"></a> [patch\_letters](#module\_patch\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/ssl | v2.0.17 |
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-ssl.zip | n/a |
## Outputs

| Name | Description |
Expand Down
7 changes: 7 additions & 0 deletions infrastructure/terraform/components/api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ locals {
})

destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"

common_db_access_lambda_env_vars = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit picky but these are common envars, but not db specific anymore

LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
LETTER_TTL_HOURS = 24,
SUPPLIER_ID_HEADER = "nhsd-supplier-id"
SUPPLIER_ID_HEADER = "nhsd-correlation-id"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module "domain_truststore" {
source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/s3bucket?ref=v2.0.17"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip"

name = "truststore"
aws_account_id = var.aws_account_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ module "get_letters" {
log_destination_arn = local.destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
LETTER_TTL_HOURS = var.letter_table_ttl_hours,
MAX_LIMIT = var.max_get_limit,
}
lambda_env_vars = merge(local.common_db_access_lambda_env_vars, {
MAX_LIMIT = var.max_get_limit
})
}

data "aws_iam_policy_document" "get_letters_lambda" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ module "patch_letters" {
log_destination_arn = local.destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
LETTER_TTL_HOURS = 24
}
lambda_env_vars = merge(local.common_db_access_lambda_env_vars, {})
}

data "aws_iam_policy_document" "patch_letters_lambda" {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module "logging_bucket" {
source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/s3bucket?ref=v2.0.17"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip"

name = "bucket-logs"
aws_account_id = var.aws_account_id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module "supplier_ssl" {
count = var.manually_configure_mtls_truststore ? 0 : 1

source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/ssl?ref=v2.0.17"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-ssl.zip"

name = "sapi_trust"
aws_account_id = var.aws_account_id
Expand Down
137 changes: 6 additions & 131 deletions infrastructure/terraform/components/api/resources/spec.tmpl.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,154 +67,29 @@
],
"patch": {
"description": "Update the status of a letter by providing the new status in the request body.",
"operationId": "patchLetters",
"requestBody": {
"content": {
"application/vnd.api+json": {
"schema": {
"properties": {
"data": {
"properties": {
"attributes": {
"properties": {
"reasonCode": {
"description": "Reason code for the given status",
"type": "number"
},
"reasonText": {
"description": "Reason text for the given status",
"type": "string"
},
"requestedProductionStatus": {
"description": "The requested production status for this letter.\nMay only be set by NHS Notify.",
"enum": [
"ACTIVE",
"HOLD",
"CANCEL"
],
"title": "ProductionStatus",
"type": "string"
},
"status": {
"default": "PENDING",
"description": "The supplier status of an individual letter",
"enum": [
"PENDING",
"ACCEPTED",
"REJECTED",
"PRINTED",
"ENCLOSED",
"CANCELLED",
"DISPATCHED",
"FAILED",
"RETURNED",
"DESTROYED",
"FORWARDED"
],
"type": "string"
}
},
"type": "object"
},
"id": {
"type": "string"
},
"type": {
"const": "Letter",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/vnd.api+json": {
"schema": {
"properties": {
"data": {
"properties": {
"attributes": {
"properties": {
"reasonCode": {
"description": "Reason code for the given status",
"type": "number"
},
"reasonText": {
"description": "Reason text for the given status",
"type": "string"
},
"requestedProductionStatus": {
"description": "A requested status for the production of a letter",
"enum": [
"ACTIVE",
"HOLD",
"CANCEL"
],
"title": "ProductionStatus",
"type": "string"
},
"status": {
"default": "PENDING",
"description": "The supplier status of an individual letter",
"enum": [
"PENDING",
"ACCEPTED",
"REJECTED",
"PRINTED",
"ENCLOSED",
"CANCELLED",
"DISPATCHED",
"FAILED",
"RETURNED",
"DESTROYED",
"FORWARDED"
],
"type": "string"
}
},
"required": [
"status",
"requestedProductionStatus"
],
"type": "object"
},
"id": {
"type": "string"
},
"type": {
"const": "Letter",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
}
},
"description": "Letter resource updated successfully"
"description": "List of letters to process"
},
"400": {
"description": "Bad request"
"description": "Bad request, invalid input data"
},
"404": {
"description": "Resource not found"
},
"500": {
"description": "Server error"
}
},
"security": [
{
"LambdaAuthorizer": []
}
],
"summary": "Update the status of a letter",
"x-amazon-apigateway-integration": {
"contentHandling": "CONVERT_TO_TEXT",
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
Expand Down
39 changes: 23 additions & 16 deletions internal/datastore/src/__test__/letter-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,23 @@ describe('LetterRepository', () => {
await db.container.stop();
});

async function checkLetterExists(supplierId: string, letterId: string) {
const letter = await letterRepository.getLetterById(supplierId, letterId);
expect(letter).toBeDefined();
expect(letter.id).toBe(letterId);
expect(letter.supplierId).toBe(supplierId);
}

async function checkLetterStatus(supplierId: string, letterId: string, status: Letter['status']) {
const letter = await letterRepository.getLetterById(supplierId, letterId);
expect(letter.status).toBe(status);
}

test('adds a letter to the database', async () => {
await letterRepository.putLetter(createLetter('supplier1', 'letter1'));
await checkLetterExists('supplier1', 'letter1');
const supplierId = 'supplier1';
const letterId = 'letter1';

await letterRepository.putLetter(createLetter(supplierId, letterId));

const letter = await letterRepository.getLetterById(supplierId, letterId);
expect(letter).toBeDefined();
expect(letter.id).toBe(letterId);
expect(letter.supplierId).toBe(supplierId);
expect(letter.reasonCode).toBeUndefined();
expect(letter.reasonText).toBeUndefined();
});

test('fetches a letter by id', async () => {
Expand Down Expand Up @@ -100,11 +102,16 @@ describe('LetterRepository', () => {
});

test('updates a letter\'s status in the database', async () => {
await letterRepository.putLetter(createLetter('supplier1', 'letter1', 'PENDING'));
const letter = createLetter('supplier1', 'letter1', 'PENDING');
await letterRepository.putLetter(letter);
await checkLetterStatus('supplier1', 'letter1', 'PENDING');

await letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED');
await checkLetterStatus('supplier1', 'letter1', 'DELIVERED');
await letterRepository.updateLetterStatus('supplier1', 'letter1', 'REJECTED', 1, "Reason text");

const updatedLetter = await letterRepository.getLetterById('supplier1', 'letter1');
expect(updatedLetter.status).toBe('REJECTED');
expect(updatedLetter.reasonCode).toBe(1);
expect(updatedLetter.reasonText).toBe('Reason text');
});

test('updates a letter\'s updatedAt date', async () => {
Expand All @@ -117,13 +124,13 @@ describe('LetterRepository', () => {
// Month is zero-indexed in JavaScript Date
// Day is one-indexed
jest.setSystemTime(new Date(2020, 1, 2));
await letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED');
await letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED', undefined, undefined);
const updatedLetter = await letterRepository.getLetterById('supplier1', 'letter1');
expect(updatedLetter.updatedAt).toBe('2020-02-02T00:00:00.000Z');
});

test('can\'t update a letter that does not exist', async () => {
await expect(letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED'))
await expect(letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED', undefined, undefined))
.rejects.toThrow('Letter with id letter1 not found for supplier supplier1');
});

Expand All @@ -132,7 +139,7 @@ describe('LetterRepository', () => {
...db.config,
lettersTableName: 'nonexistent-table'
});
await expect(misconfiguredRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED'))
await expect(misconfiguredRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED', undefined, undefined))
.rejects.toThrow('Cannot do operations on a non-existent table');
});

Expand All @@ -157,7 +164,7 @@ describe('LetterRepository', () => {
const pendingLetters = await letterRepository.getLettersByStatus('supplier1', 'PENDING');
expect(pendingLetters.letters).toHaveLength(2);

await letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED');
await letterRepository.updateLetterStatus('supplier1', 'letter1', 'DELIVERED', undefined, undefined);
const remainingLetters = await letterRepository.getLettersByStatus('supplier1', 'PENDING');
expect(remainingLetters.letters).toHaveLength(1);
expect(remainingLetters.letters[0].id).toBe('letter2');
Expand Down
Loading
Loading