diff --git a/.github/scripts/dispatch_internal_repo_workflow.sh b/.github/scripts/dispatch_internal_repo_workflow.sh index 714a3fed5..dd7b1a540 100755 --- a/.github/scripts/dispatch_internal_repo_workflow.sh +++ b/.github/scripts/dispatch_internal_repo_workflow.sh @@ -104,6 +104,14 @@ while [[ $# -gt 0 ]]; do version="$2" shift 2 ;; + --tableName) # Table name (optional) + tableName="$2" + shift 2 + ;; + --force) # Force apply flag (optional) + force="$2" + shift 2 + ;; *) echo "[ERROR] Unknown argument: $1" exit 1 @@ -202,6 +210,14 @@ if [[ -z "$version" ]]; then version="" fi +if [{ -z "$tableName" }]; then + tableName="" +fi + +if [[ -z "$force" ]]; then + force="" +fi + echo "==================== Workflow Dispatch Parameters ====================" echo " infraRepoName: $infraRepoName" echo " releaseVersion: $releaseVersion" @@ -221,6 +237,8 @@ echo " apimEnvironment: $apimEnvironment" echo " boundedContext: $boundedContext" echo " targetDomain: $targetDomain" echo " version: $version" +echo " tableName: $tableName" +echo " force: $force" DISPATCH_EVENT=$(jq -ncM \ --arg infraRepoName "$infraRepoName" \ @@ -240,6 +258,8 @@ DISPATCH_EVENT=$(jq -ncM \ --arg boundedContext "$boundedContext" \ --arg targetDomain "$targetDomain" \ --arg version "$version" \ + --arg tableName "$tableName" \ + --arg force "$force" \ '{ "ref": "'"$internalRef"'", "inputs": ( @@ -255,6 +275,8 @@ DISPATCH_EVENT=$(jq -ncM \ (if $boundedContext != "" then { "boundedContext": $boundedContext } else {} end) + (if $targetDomain != "" then { "targetDomain": $targetDomain } else {} end) + (if $version != "" then { "version": $version } else {} end) + + (if $tableName != "" then { "tableName": $tableName } else {} end) + + (if $force != "" then { "force": $force } else {} end) + (if $targetAccountGroup != "" then { "targetAccountGroup": $targetAccountGroup } else {} end) + { "releaseVersion": $releaseVersion, diff --git a/.github/workflows/stage-3-build.yaml b/.github/workflows/stage-3-build.yaml index 3d476c2d6..ca92c27a8 100644 --- a/.github/workflows/stage-3-build.yaml +++ b/.github/workflows/stage-3-build.yaml @@ -162,6 +162,34 @@ jobs: --terraformAction "apply" \ --overrideProjectName "nhs" \ --overrideRoleName "nhs-main-acct-supplier-api-github-deploy" + populate-config: + name: "Populate Supplier Config" + runs-on: ubuntu-latest + needs: [pr-create-dynamic-environment] + timeout-minutes: 10 + + steps: + - name: "Checkout code" + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - name: Set Environment Name + id: set-environment + run: echo "environment_name=${{ inputs.pr_number != '' && format('pr{0}', inputs.pr_number) || 'main' }}" >> $GITHUB_OUTPUT + - name: "Trigger populate config workflow in internal repo" + env: + APP_CLIENT_ID: ${{ secrets.APP_CLIENT_ID }} + APP_PEM_FILE: ${{ secrets.APP_PEM_FILE }} + PR_NUMBER: ${{ inputs.pr_number }} + shell: bash + run: | + .github/scripts/dispatch_internal_repo_workflow.sh \ + --infraRepoName "$(echo ${{ github.repository }} | cut -d'/' -f2)" \ + --releaseVersion ${{ github.head_ref || github.ref_name }} \ + --targetWorkflow "publish-supplier-config.yaml" \ + --targetEnvironment "${{ steps.set-environment.outputs.environment_name }}" \ + --targetComponent "config" \ + --targetAccountGroup "nhs-notify-suppliers-dev" \ + --tableName "supplier-config" \ + --force "true" artefact-proxies: name: "Build proxies" runs-on: ubuntu-latest diff --git a/config/suppliers/letter-variant/client1-campaign.json b/config/suppliers/letter-variant/client1-campaign.json new file mode 100644 index 000000000..0843d9375 --- /dev/null +++ b/config/suppliers/letter-variant/client1-campaign.json @@ -0,0 +1,37 @@ +{ + "campaignIds": [ + "client1-campaign" + ], + "clientId": "client1", + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "colourCoveragePercentage": { + "operator": "LESS_THAN", + "value": 10 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 3 + }, + "sides": { + "operator": "LESS_THAN", + "value": 6 + } + }, + "description": "Colour printing, campaign envelope, Attachment", + "id": "client1-campaign", + "name": "Client1 - campaign", + "packSpecificationIds": [ + "client1-campaign" + ], + "status": "INT", + "type": "STANDARD", + "volumeGroupId": "volumeGroup-test3" +} diff --git a/config/suppliers/letter-variant/notify-standard-test1.json b/config/suppliers/letter-variant/notify-standard-test1.json new file mode 100644 index 000000000..dda16237b --- /dev/null +++ b/config/suppliers/letter-variant/notify-standard-test1.json @@ -0,0 +1,30 @@ +{ + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 5 + }, + "sides": { + "operator": "LESS_THAN", + "value": 10 + } + }, + "description": "Dev test variant for happy path testing", + "id": "notify-standard-test1", + "name": "Dev Happy Path", + "packSpecificationIds": [ + "notify-c5", + "notify-c4" + ], + "status": "PROD", + "type": "STANDARD", + "volumeGroupId": "volumeGroup-test1" +} diff --git a/config/suppliers/letter-variant/notify-standard.json b/config/suppliers/letter-variant/notify-standard.json new file mode 100644 index 000000000..9f1ec1504 --- /dev/null +++ b/config/suppliers/letter-variant/notify-standard.json @@ -0,0 +1,29 @@ +{ + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 5 + }, + "sides": { + "operator": "LESS_THAN", + "value": 10 + } + }, + "description": "Black printing, economy postage tariff", + "id": "notify-standard", + "name": "Standard Letter", + "packSpecificationIds": [ + "notify-c5" + ], + "status": "PROD", + "type": "STANDARD", + "volumeGroupId": "volumeGroup-test2" +} diff --git a/config/suppliers/pack-specification/client1-campaign.json b/config/suppliers/pack-specification/client1-campaign.json new file mode 100644 index 000000000..e00d68f9a --- /dev/null +++ b/config/suppliers/pack-specification/client1-campaign.json @@ -0,0 +1,58 @@ +{ + "assembly": { + "duplex": true, + "envelopeId": "client1-campaign", + "features": [ + "ADMAIL" + ], + "insertIds": [ + "client1-campaign-fast-facts-attachment" + ], + "paper": { + "colour": "WHITE", + "id": "paper-std-white-100", + "name": "Standard White 80gsm", + "recycled": true, + "size": "A4", + "weightGSM": 80 + }, + "printColour": "COLOUR" + }, + "billingId": "client1-campaign-billing", + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "colourCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 4 + }, + "sides": { + "operator": "LESS_THAN", + "value": 8 + } + }, + "createdAt": "2026-01-12T00:00:00.000Z", + "description": "Envelope and attachment for campaign", + "id": "client1-campaign", + "name": "Client1 - campaign", + "postage": { + "deliveryDays": 3, + "id": "economy", + "maxThicknessMm": 5, + "maxWeightGrams": 100, + "size": "STANDARD" + }, + "status": "PROD", + "updatedAt": "2026-02-16T17:00:00.000Z", + "version": 2 +} diff --git a/config/suppliers/pack-specification/notify-c4.json b/config/suppliers/pack-specification/notify-c4.json new file mode 100644 index 000000000..dc42c0f1b --- /dev/null +++ b/config/suppliers/pack-specification/notify-c4.json @@ -0,0 +1,48 @@ +{ + "assembly": { + "duplex": true, + "envelopeId": "economy-c5", + "paper": { + "colour": "WHITE", + "id": "paper-std-white-80", + "name": "Standard White 80gsm", + "recycled": true, + "size": "A4", + "weightGSM": 80 + }, + "printColour": "BLACK" + }, + "billingId": "notify-c4", + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 20 + }, + "sides": { + "operator": "LESS_THAN", + "value": 40 + } + }, + "createdAt": "2026-01-12T00:00:00.000Z", + "description": "C4 pack (large letter)", + "id": "notify-c4", + "name": "Notify standard (C4)", + "postage": { + "deliveryDays": 3, + "id": "economy", + "maxThicknessMm": 25, + "maxWeightGrams": 500, + "size": "LARGE" + }, + "status": "DRAFT", + "updatedAt": "2026-01-12T00:00:00.000Z", + "version": 1 +} diff --git a/config/suppliers/pack-specification/notify-c5.json b/config/suppliers/pack-specification/notify-c5.json new file mode 100644 index 000000000..2a4024a91 --- /dev/null +++ b/config/suppliers/pack-specification/notify-c5.json @@ -0,0 +1,48 @@ +{ + "assembly": { + "duplex": true, + "envelopeId": "economy-c4", + "paper": { + "colour": "WHITE", + "id": "paper-std-white-80", + "name": "Standard White 80gsm", + "recycled": true, + "size": "A4", + "weightGSM": 80 + }, + "printColour": "BLACK" + }, + "billingId": "notify-c5-billing", + "constraints": { + "blackCoveragePercentage": { + "operator": "LESS_THAN", + "value": 20 + }, + "deliveryDays": { + "operator": "LESS_THAN", + "value": 3 + }, + "sheets": { + "operator": "LESS_THAN", + "value": 5 + }, + "sides": { + "operator": "LESS_THAN", + "value": 10 + } + }, + "createdAt": "2026-01-12T00:00:00.000Z", + "description": "C5 pack", + "id": "notify-c5", + "name": "Notify standard", + "postage": { + "deliveryDays": 3, + "id": "economy", + "maxThicknessMm": 5, + "maxWeightGrams": 100, + "size": "STANDARD" + }, + "status": "PROD", + "updatedAt": "2026-01-12T00:00:00.000Z", + "version": 1 +} diff --git a/config/suppliers/supplier-allocation/supplier1-volumeGroup-test1.json b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test1.json new file mode 100644 index 000000000..81a82b31b --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test1.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 30, + "id": "supplier1-volumeGroup-test1", + "status": "PROD", + "supplier": "supplier1", + "volumeGroup": "volumeGroup-test1" +} diff --git a/config/suppliers/supplier-allocation/supplier1-volumeGroup-test2.json b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test2.json new file mode 100644 index 000000000..cada2db0a --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test2.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 100, + "id": "supplier1-volumeGroup-test2", + "status": "PROD", + "supplier": "supplier1", + "volumeGroup": "volumeGroup-test2" +} diff --git a/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json new file mode 100644 index 000000000..f08467c9f --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier1-volumeGroup-test3.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 100, + "id": "supplier1-volumeGroup-test3", + "status": "PROD", + "supplier": "supplier1", + "volumeGroup": "volumeGroup-test3" +} diff --git a/config/suppliers/supplier-allocation/supplier2-volumeGroup-test1.json b/config/suppliers/supplier-allocation/supplier2-volumeGroup-test1.json new file mode 100644 index 000000000..9d34abba3 --- /dev/null +++ b/config/suppliers/supplier-allocation/supplier2-volumeGroup-test1.json @@ -0,0 +1,7 @@ +{ + "allocationPercentage": 70, + "id": "supplier2-volumeGroup-test1", + "status": "PROD", + "supplier": "supplier2", + "volumeGroup": "volumeGroup-test1" +} diff --git a/config/suppliers/supplier-pack/supplier1-client1-campaign.json b/config/suppliers/supplier-pack/supplier1-client1-campaign.json new file mode 100644 index 000000000..3340acdb1 --- /dev/null +++ b/config/suppliers/supplier-pack/supplier1-client1-campaign.json @@ -0,0 +1,7 @@ +{ + "approval": "APPROVED", + "id": "supplier1-client1-campaign", + "packSpecificationId": "client1-campaign", + "status": "PROD", + "supplierId": "supplier1" +} diff --git a/config/suppliers/supplier-pack/supplier1-notify-c4.json b/config/suppliers/supplier-pack/supplier1-notify-c4.json new file mode 100644 index 000000000..c804a0d5a --- /dev/null +++ b/config/suppliers/supplier-pack/supplier1-notify-c4.json @@ -0,0 +1,7 @@ +{ + "approval": "DRAFT", + "id": "supplier1-notify-c4", + "packSpecificationId": "notify-c4", + "status": "DRAFT", + "supplierId": "supplier1" +} diff --git a/config/suppliers/supplier-pack/supplier1-notify-c5.json b/config/suppliers/supplier-pack/supplier1-notify-c5.json new file mode 100644 index 000000000..1b49bce1d --- /dev/null +++ b/config/suppliers/supplier-pack/supplier1-notify-c5.json @@ -0,0 +1,7 @@ +{ + "approval": "APPROVED", + "id": "supplier1-notify-c5", + "packSpecificationId": "notify-c5", + "status": "PROD", + "supplierId": "supplier1" +} diff --git a/config/suppliers/supplier/supplier1.json b/config/suppliers/supplier/supplier1.json new file mode 100644 index 000000000..5ef81166a --- /dev/null +++ b/config/suppliers/supplier/supplier1.json @@ -0,0 +1,7 @@ +{ + "channelType": "LETTER", + "dailyCapacity": 500000, + "id": "supplier1", + "name": "Supplier1", + "status": "PROD" +} diff --git a/config/suppliers/supplier/supplier2.json b/config/suppliers/supplier/supplier2.json new file mode 100644 index 000000000..883da65cc --- /dev/null +++ b/config/suppliers/supplier/supplier2.json @@ -0,0 +1,7 @@ +{ + "channelType": "LETTER", + "dailyCapacity": 500000, + "id": "supplier2", + "name": "Supplier2", + "status": "PROD" +} diff --git a/config/suppliers/volume-group/volumeGroup-test1.json b/config/suppliers/volume-group/volumeGroup-test1.json new file mode 100644 index 000000000..3b3bab804 --- /dev/null +++ b/config/suppliers/volume-group/volumeGroup-test1.json @@ -0,0 +1,7 @@ +{ + "description": "Dev Test Volume Group 1", + "id": "volumeGroup-test1", + "name": "Dev Test Volume Group 1", + "startDate": "2026-01-01", + "status": "PROD" +} diff --git a/config/suppliers/volume-group/volumeGroup-test2.json b/config/suppliers/volume-group/volumeGroup-test2.json new file mode 100644 index 000000000..0eb829ec2 --- /dev/null +++ b/config/suppliers/volume-group/volumeGroup-test2.json @@ -0,0 +1,7 @@ +{ + "description": "Dev Test Volume Group 2", + "id": "volumeGroup-test2", + "name": "Dev Test Volume Group 2", + "startDate": "2026-01-01", + "status": "PROD" +} diff --git a/config/suppliers/volume-group/volumeGroup-test3.json b/config/suppliers/volume-group/volumeGroup-test3.json new file mode 100644 index 000000000..650199fe3 --- /dev/null +++ b/config/suppliers/volume-group/volumeGroup-test3.json @@ -0,0 +1,7 @@ +{ + "description": "Dev Test Volume Group 3", + "id": "volumeGroup-test3", + "name": "Dev Test Volume Group 3", + "startDate": "2026-01-01", + "status": "PROD" +} diff --git a/infrastructure/terraform/components/api/ddb_table_supplier_configuration.tf b/infrastructure/terraform/components/api/ddb_table_supplier_configuration.tf index 8d089a19b..f751e2ef4 100644 --- a/infrastructure/terraform/components/api/ddb_table_supplier_configuration.tf +++ b/infrastructure/terraform/components/api/ddb_table_supplier_configuration.tf @@ -2,8 +2,8 @@ resource "aws_dynamodb_table" "supplier-configuration" { name = "${local.csi}-supplier-config" billing_mode = "PAY_PER_REQUEST" - hash_key = "PK" - range_key = "SK" + hash_key = "pk" + range_key = "sk" ttl { attribute_name = "ttl" @@ -11,17 +11,12 @@ resource "aws_dynamodb_table" "supplier-configuration" { } attribute { - name = "PK" + name = "pk" type = "S" } attribute { - name = "SK" - type = "S" - } - - attribute { - name = "entityType" + name = "sk" type = "S" } @@ -30,17 +25,9 @@ resource "aws_dynamodb_table" "supplier-configuration" { type = "S" } - // The type-index GSI allows us to query for all supplier configurations of a given type (e.g. all letter supplier configurations) - global_secondary_index { - name = "EntityTypeIndex" - hash_key = "entityType" - range_key = "SK" - projection_type = "ALL" - } - global_secondary_index { name = "volumeGroup-index" - hash_key = "PK" + hash_key = "pk" range_key = "volumeGroup" projection_type = "ALL" } diff --git a/internal/datastore/src/__test__/db.ts b/internal/datastore/src/__test__/db.ts index 481166e60..9d0bf0e1e 100644 --- a/internal/datastore/src/__test__/db.ts +++ b/internal/datastore/src/__test__/db.ts @@ -151,14 +151,14 @@ const createSupplierConfigTableCommand = new CreateTableCommand({ TableName: "supplier-config", BillingMode: "PAY_PER_REQUEST", KeySchema: [ - { AttributeName: "PK", KeyType: "HASH" }, // Partition key - { AttributeName: "SK", KeyType: "RANGE" }, // Sort key + { AttributeName: "pk", KeyType: "HASH" }, // Partition key + { AttributeName: "sk", KeyType: "RANGE" }, // Sort key ], GlobalSecondaryIndexes: [ { IndexName: "volumeGroup-index", KeySchema: [ - { AttributeName: "PK", KeyType: "HASH" }, // Partition key for GSI + { AttributeName: "pk", KeyType: "HASH" }, // Partition key for GSI { AttributeName: "volumeGroup", KeyType: "RANGE" }, // Sort key for GSI ], Projection: { @@ -167,8 +167,8 @@ const createSupplierConfigTableCommand = new CreateTableCommand({ }, ], AttributeDefinitions: [ - { AttributeName: "PK", AttributeType: "S" }, - { AttributeName: "SK", AttributeType: "S" }, + { AttributeName: "pk", AttributeType: "S" }, + { AttributeName: "sk", AttributeType: "S" }, { AttributeName: "volumeGroup", AttributeType: "S" }, ], }); diff --git a/internal/datastore/src/__test__/supplier-config-repository.test.ts b/internal/datastore/src/__test__/supplier-config-repository.test.ts index 9fde74f94..ddd44fd4d 100644 --- a/internal/datastore/src/__test__/supplier-config-repository.test.ts +++ b/internal/datastore/src/__test__/supplier-config-repository.test.ts @@ -9,8 +9,8 @@ import { SupplierConfigRepository } from "../supplier-config-repository"; function createLetterVariantItem(variantId: string) { return { - PK: "LETTER_VARIANT", - SK: variantId, + pk: "ENTITY#letter-variant", + sk: `ID#${variantId}`, id: variantId, name: `Variant ${variantId}`, description: `Description for variant ${variantId}`, @@ -29,8 +29,8 @@ function createVolumeGroupItem(groupId: string, status = "PROD") { .toISOString() .split("T")[0]; // Ends in a day to ensure it's active based on end date. Tests can override this if needed. return { - PK: "VOLUME_GROUP", - SK: groupId, + pk: "ENTITY#volume-group", + sk: `ID#${groupId}`, id: groupId, name: `Volume Group ${groupId}`, description: `Description for volume group ${groupId}`, @@ -46,8 +46,8 @@ function createSupplierAllocationItem( supplier: string, ) { return { - PK: `SUPPLIER_ALLOCATION`, - SK: allocationId, + pk: "ENTITY#supplier-allocation", + sk: `ID#${allocationId}`, id: allocationId, status: "PROD", volumeGroup: groupId, @@ -58,8 +58,8 @@ function createSupplierAllocationItem( function createSupplierItem(supplierId: string) { return { - PK: "SUPPLIER", - SK: supplierId, + pk: "ENTITY#supplier", + sk: `ID#${supplierId}`, id: supplierId, name: `Supplier ${supplierId}`, channelType: "LETTER", diff --git a/internal/datastore/src/supplier-config-repository.ts b/internal/datastore/src/supplier-config-repository.ts index 1f82bb0c2..4eeeddb10 100644 --- a/internal/datastore/src/supplier-config-repository.ts +++ b/internal/datastore/src/supplier-config-repository.ts @@ -28,7 +28,7 @@ export class SupplierConfigRepository { const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.supplierConfigTableName, - Key: { PK: "LETTER_VARIANT", SK: variantId }, + Key: { pk: "ENTITY#letter-variant", sk: `ID#${variantId}` }, }), ); if (!result.Item) { @@ -42,7 +42,7 @@ export class SupplierConfigRepository { const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.supplierConfigTableName, - Key: { PK: "VOLUME_GROUP", SK: groupId }, + Key: { pk: "ENTITY#volume-group", sk: `ID#${groupId}` }, }), ); if (!result.Item) { @@ -61,12 +61,12 @@ export class SupplierConfigRepository { KeyConditionExpression: "#pk = :pk AND #group = :groupId", FilterExpression: "#status = :status ", ExpressionAttributeNames: { - "#pk": "PK", + "#pk": "pk", "#group": "volumeGroup", "#status": "status", }, ExpressionAttributeValues: { - ":pk": "SUPPLIER_ALLOCATION", + ":pk": "ENTITY#supplier-allocation", ":groupId": groupId, ":status": "PROD", }, @@ -87,7 +87,7 @@ export class SupplierConfigRepository { const result = await this.ddbClient.send( new GetCommand({ TableName: this.config.supplierConfigTableName, - Key: { PK: "SUPPLIER", SK: supplierId }, + Key: { pk: "ENTITY#supplier", sk: `ID#${supplierId}` }, }), ); if (!result.Item) {