diff --git a/.github/scripts/release_code.sh b/.github/scripts/release_code.sh index 46709d1a7f..1f11999ac6 100755 --- a/.github/scripts/release_code.sh +++ b/.github/scripts/release_code.sh @@ -61,6 +61,7 @@ sam deploy \ LogRetentionInDays="$LOG_RETENTION_DAYS" \ Environment="$TARGET_ENVIRONMENT" \ DeployCheckPrescriptionStatusUpdate="$DEPLOY_CHECK_PRESCRIPTION_STATUS_UPDATE" \ + DeployNotificationsReporting="$DEPLOY_NOTIFICATIONS_REPORTING" \ EnableAlerts="$ENABLE_ALERTS" \ StateMachineLogLevel="$STATE_MACHINE_LOG_LEVEL" \ EnableNotificationsInternal="$ENABLE_NOTIFICATIONS_INTERNAL" \ diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 99071d49d3..4811536be4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -131,6 +131,7 @@ jobs: TEST_PRESCRIPTIONS_3: ${{ vars.TEST_PRESCRIPTIONS_3_VALUES }} TEST_PRESCRIPTIONS_4: ${{ vars.TEST_PRESCRIPTIONS_4_VALUES }} FORWARD_CSOC_LOGS: false + DEPLOY_NOTIFICATIONS_REPORTING_ENDPOINT: true secrets: CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} @@ -166,6 +167,7 @@ jobs: MTLS_KEY: psu-mtls-1 IS_PULL_REQUEST: true FORWARD_CSOC_LOGS: false + DEPLOY_NOTIFICATIONS_REPORTING_ENDPOINT: true secrets: CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} diff --git a/.github/workflows/run_release_code_and_api.yml b/.github/workflows/run_release_code_and_api.yml index 0ee15ac0e1..b1fce854bd 100644 --- a/.github/workflows/run_release_code_and_api.yml +++ b/.github/workflows/run_release_code_and_api.yml @@ -124,6 +124,9 @@ on: FORWARD_CSOC_LOGS: required: true type: boolean + DEPLOY_NOTIFICATIONS_REPORTING_ENDPOINT: + type: boolean + default: false secrets: CLOUD_FORMATION_DEPLOY_ROLE: required: true @@ -231,6 +234,7 @@ jobs: TEST_PRESCRIPTIONS_3: ${{ inputs.TEST_PRESCRIPTIONS_3 || 'noval' }} TEST_PRESCRIPTIONS_4: ${{ inputs.TEST_PRESCRIPTIONS_4 || 'noval' }} FORWARD_CSOC_LOGS: ${{ inputs.FORWARD_CSOC_LOGS }} + DEPLOY_NOTIFICATIONS_REPORTING: ${{ inputs.DEPLOY_NOTIFICATIONS_REPORTING_ENDPOINT }} run: ./release_code.sh - name: get mtls secrets diff --git a/.vscode/eps-prescription-status-update-api.code-workspace b/.vscode/eps-prescription-status-update-api.code-workspace index 06664a3e91..31cbdef70d 100644 --- a/.vscode/eps-prescription-status-update-api.code-workspace +++ b/.vscode/eps-prescription-status-update-api.code-workspace @@ -32,6 +32,10 @@ "name": "packages/nhsNotifyLambda", "path": "../packages/nhsNotifyLambda" }, + { + "name": "packages/notificationsReportingLambda", + "path": "../packages/notificationsReportingLambda" + }, { "name": "packages/postDatedLambda", "path": "../packages/postDatedLambda" @@ -156,8 +160,14 @@ "versionable", "whens" ], - "cSpell.dictionaries": ["en-GB"], - "cSpell.ignorePaths": ["package-lock.json", "node_modules", ".vscode"], + "cSpell.dictionaries": [ + "en-GB" + ], + "cSpell.ignorePaths": [ + "package-lock.json", + "node_modules", + ".vscode" + ], "jest.jestCommandLine": "NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest --no-cache", "jest.nodeEnv": { "POWERTOOLS_DEV": true diff --git a/Makefile b/Makefile index 8acae25488..dced90dd9e 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ sam-sync: guard-AWS_DEFAULT_PROFILE guard-stack_name compile LogLevel=$${LOG_LEVEL:-INFO} \ LogRetentionInDays=$${LOG_RETENTION_DAYS:-30} \ DeployCheckPrescriptionStatusUpdate=true \ + DeployNotificationsReporting=true \ EnableAlerts=false \ Environment=$$AWS_ENVIRONMENT \ StateMachineLogLevel=$${STATE_MACHINE_LOG_LEVEL:-ALL} \ @@ -74,6 +75,7 @@ sam-deploy: guard-AWS_DEFAULT_PROFILE guard-stack_name LogLevel=$${LOG_LEVEL:-INFO} \ LogRetentionInDays=$${LOG_RETENTION_DAYS:-30} \ DeployCheckPrescriptionStatusUpdate=true \ + DeployNotificationsReporting=true \ EnableAlerts=false \ Environment=$$AWS_ENVIRONMENT \ StateMachineLogLevel=$${STATE_MACHINE_LOG_LEVEL:-ALL} \ @@ -168,6 +170,7 @@ lint-node: compile-node npm run lint --workspace packages/cpsuLambda npm run lint --workspace packages/checkPrescriptionStatusUpdates npm run lint --workspace packages/nhsNotifyLambda + npm run lint --workspace packages/notificationsReportingLambda npm run lint --workspace packages/postDatedLambda npm run lint --workspace packages/nhsNotifyUpdateCallback npm run lint --workspace packages/common/testing @@ -201,6 +204,7 @@ test: compile npm run test --workspace packages/cpsuLambda npm run test --workspace packages/checkPrescriptionStatusUpdates npm run test --workspace packages/nhsNotifyLambda + npm run test --workspace packages/notificationsReportingLambda npm run test --workspace packages/postDatedLambda npm run test --workspace packages/nhsNotifyUpdateCallback npm run test --workspace packages/common/middyErrorHandler @@ -222,6 +226,8 @@ clean: rm -rf packages/cpsuLambda/lib rm -rf packages/nhsNotifyLambda/coverage rm -rf packages/nhsNotifyLambda/lib + rm -rf packages/notificationsReportingLambda/coverage + rm -rf packages/notificationsReportingLambda/lib rm -rf packages/postDatedLambda/coverage rm -rf packages/postDatedLambda/lib rm -rf packages/nhsNotifyUpdateCallback/coverage diff --git a/SAMtemplates/apis/main.yaml b/SAMtemplates/apis/main.yaml index 6a3e97ece1..57e6c0a4e3 100644 --- a/SAMtemplates/apis/main.yaml +++ b/SAMtemplates/apis/main.yaml @@ -62,6 +62,14 @@ Parameters: Type: String Default: none + NotificationsReportingFunctionName: + Type: String + Default: none + + NotificationsReportingFunctionArn: + Type: String + Default: none + LogRetentionInDays: Type: Number @@ -71,6 +79,10 @@ Parameters: DeployCheckPrescriptionStatusUpdate: Type: String + DeployNotificationsReporting: + Type: String + Default: false + ForwardCsocLogs: Type: String Default: false @@ -81,6 +93,9 @@ Conditions: ShouldDeployCheckPrescriptionStatusUpdate: !Equals - true - !Ref DeployCheckPrescriptionStatusUpdate + ShouldDeployNotificationsReporting: !Equals + - true + - !Ref DeployNotificationsReporting Resources: GenerateCertificate: @@ -549,11 +564,53 @@ Resources: Timeout: "1" Count: 0 + NotificationsReportingResource: + Condition: ShouldDeployNotificationsReporting + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref RestApiGateway + ParentId: !GetAtt RestApiGateway.RootResourceId + PathPart: notifications-reporting + + NotificationsReportingMethod: + Condition: ShouldDeployNotificationsReporting + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref RestApiGateway + ResourceId: !Ref NotificationsReportingResource + HttpMethod: GET + AuthorizationType: NONE + Integration: + Type: AWS_PROXY + Credentials: !GetAtt RestApiGatewayResources.Outputs.ApiGwRoleArn + IntegrationHttpMethod: POST + Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${NotificationsReportingFunctionArn}/invocations + + NotificationsReportingHandle: + Condition: ShouldDeployNotificationsReporting + DependsOn: NotificationsReportingMethod + Type: "AWS::CloudFormation::WaitConditionHandle" + + NotificationsReportingWaitHandle: + Type: "AWS::CloudFormation::WaitConditionHandle" + + NotificationsReportingWaitCondition: + Type: "AWS::CloudFormation::WaitCondition" + Properties: + Handle: + !If [ + ShouldDeployNotificationsReporting, + !Ref NotificationsReportingHandle, + !Ref NotificationsReportingWaitHandle, + ] + Timeout: "1" + Count: 0 + # ********************************************************************* # if you add a new endpoint, then change the name of this resource # also need to change it in RestApiGatewayStage.Properties.DeploymentId # ********************************************************************* - RestApiGatewayDeploymentV2f: + RestApiGatewayDeploymentV2g: Type: AWS::ApiGateway::Deployment DependsOn: # see note above if you add something in here when you add a new endpoint @@ -563,6 +620,7 @@ Resources: - Format1UpdatePrescriptionStatusMethod - CheckPrescriptionStatusUpdatesWaitCondition - NotificationDeliveryStatusCallbackMethod + - NotificationsReportingMethod # see note above if you add something in here when you add a new endpoint Properties: RestApiId: !Ref RestApiGateway @@ -571,7 +629,7 @@ Resources: Type: AWS::ApiGateway::Stage Properties: RestApiId: !Ref RestApiGateway - DeploymentId: !Ref RestApiGatewayDeploymentV2f + DeploymentId: !Ref RestApiGatewayDeploymentV2g StageName: prod TracingEnabled: true AccessLogSetting: @@ -600,6 +658,10 @@ Resources: - ShouldDeployCheckPrescriptionStatusUpdate - Fn::ImportValue: !Sub ${StackName}:functions:${CheckPrescriptionStatusUpdatesFunctionName}:ExecuteLambdaPolicyArn - !Ref AWS::NoValue + - !If + - ShouldDeployNotificationsReporting + - Fn::ImportValue: !Sub ${StackName}:functions:${NotificationsReportingFunctionName}:ExecuteLambdaPolicyArn + - !Ref AWS::NoValue ApiName: !Sub ${StackName}-apigw LogRetentionInDays: !Ref LogRetentionInDays diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index 2c8868377a..5c3a2f741c 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -90,6 +90,10 @@ Parameters: DeployCheckPrescriptionStatusUpdate: Type: String + DeployNotificationsReporting: + Type: String + Default: false + Environment: Type: String @@ -113,6 +117,10 @@ Conditions: - true - !Ref DeployCheckPrescriptionStatusUpdate + ShouldDeployNotificationsReporting: !Equals + - true + - !Ref DeployNotificationsReporting + EnableBackupCondition: !Equals - "True" - !Ref EnableBackup @@ -402,6 +410,53 @@ Resources: SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream + NotificationsReporting: + Condition: ShouldDeployNotificationsReporting + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub ${StackName}-NotificationsReporting + CodeUri: ../../packages + Handler: notificationsReportingLambda.handler + Role: !GetAtt NotificationsReportingResources.Outputs.LambdaRoleArn + Environment: + Variables: + TABLE_NAME: !Ref PrescriptionNotificationStatesTableName + Metadata: + BuildMethod: esbuild + guard: + SuppressedRules: + - LAMBDA_DLQ_CHECK + - LAMBDA_INSIDE_VPC + - LAMBDA_CONCURRENCY_CHECK + BuildProperties: + Minify: true + Target: es2020 + Sourcemap: true + tsconfig: notificationsReportingLambda/tsconfig.json + packages: bundle + EntryPoints: + - notificationsReportingLambda/src/notificationsReportingLambda.ts + + NotificationsReportingResources: + Condition: ShouldDeployNotificationsReporting + Type: AWS::Serverless::Application + Properties: + Location: lambda_resources.yaml + Parameters: + StackName: !Ref StackName + LambdaName: !Sub ${StackName}-NotificationsReporting + LambdaArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${StackName}-NotificationsReporting + IncludeAdditionalPolicies: true + AdditionalPolicies: !Join + - "," + - - Fn::ImportValue: !Sub ${StackName}:tables:${PrescriptionNotificationStatesTableName}:TableReadPolicyArn + - Fn::ImportValue: !Sub ${StackName}:tables:UsePrescriptionNotificationStatesKMSKeyPolicyArn + LogRetentionInDays: !Ref LogRetentionInDays + CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn + EnableSplunk: !Ref EnableSplunk + SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole + SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream + NotifyProcessorScheduleEventRole: Type: AWS::IAM::Role Properties: @@ -823,6 +878,20 @@ Outputs: Description: The function ARN of the NHSNotifyUpdateCallback lambda Value: !GetAtt NHSNotifyUpdateCallback.Arn + NotificationsReportingFunctionName: + Description: The function name of the NotificationsReporting lambda + Value: !If + - ShouldDeployNotificationsReporting + - !Ref NotificationsReporting + - "" + + NotificationsReportingFunctionArn: + Description: The function ARN of the NotificationsReporting lambda + Value: !If + - ShouldDeployNotificationsReporting + - !GetAtt NotificationsReporting.Arn + - "" + # Post-dated lambda outputs PostDatedNotifyLambdaFunctionName: Description: The function name of the postDatedNotifyLambda diff --git a/SAMtemplates/main_template.yaml b/SAMtemplates/main_template.yaml index 08c15fcf7a..82ff77b2f5 100644 --- a/SAMtemplates/main_template.yaml +++ b/SAMtemplates/main_template.yaml @@ -71,6 +71,10 @@ Parameters: DeployCheckPrescriptionStatusUpdate: Type: String + DeployNotificationsReporting: + Type: String + Default: false + Environment: Type: String @@ -225,9 +229,12 @@ Resources: CheckPrescriptionStatusUpdatesFunctionArn: !GetAtt Functions.Outputs.CheckPrescriptionStatusUpdatesFunctionArn NHSNotifyUpdateCallbackFunctionName: !GetAtt Functions.Outputs.NHSNotifyUpdateCallbackFunctionName NHSNotifyUpdateCallbackFunctionArn: !GetAtt Functions.Outputs.NHSNotifyUpdateCallbackFunctionArn + NotificationsReportingFunctionName: !GetAtt Functions.Outputs.NotificationsReportingFunctionName + NotificationsReportingFunctionArn: !GetAtt Functions.Outputs.NotificationsReportingFunctionArn LogRetentionInDays: !Ref LogRetentionInDays EnableSplunk: !Ref EnableSplunk DeployCheckPrescriptionStatusUpdate: !Ref DeployCheckPrescriptionStatusUpdate + DeployNotificationsReporting: !Ref DeployNotificationsReporting ForwardCsocLogs: !Ref ForwardCsocLogs Functions: @@ -255,6 +262,7 @@ Resources: VersionNumber: !Ref VersionNumber CommitId: !Ref CommitId DeployCheckPrescriptionStatusUpdate: !Ref DeployCheckPrescriptionStatusUpdate + DeployNotificationsReporting: !Ref DeployNotificationsReporting Environment: !Ref Environment EnableBackup: !Ref EnableBackup RequireApplicationName: !Ref RequireApplicationName diff --git a/package-lock.json b/package-lock.json index 59292f518f..2d6c6b431f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "packages/cpsuLambda", "packages/checkPrescriptionStatusUpdates", "packages/nhsNotifyLambda", + "packages/notificationsReportingLambda", "packages/postDatedLambda", "packages/psuRestoreValidationLambda", "packages/nhsNotifyUpdateCallback", @@ -160,8 +161,6 @@ }, "node_modules/@aws-lambda-powertools/commons": { "version": "2.31.0", - "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/commons/-/commons-2.31.0.tgz", - "integrity": "sha512-K5Hy3WtU/n0M8/kgsgw6OVlF709PHOfnTfsWrJS2Cs1vVjEeBvW22a6u9n3VPNpDeqJ7GIodmSkbuzvQzq6vaA==", "license": "MIT-0", "dependencies": { "@aws/lambda-invoke-store": "0.2.3" @@ -169,8 +168,6 @@ }, "node_modules/@aws-lambda-powertools/logger": { "version": "2.31.0", - "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/logger/-/logger-2.31.0.tgz", - "integrity": "sha512-FCchy2g/cK5poDvDWnVCBSN9uSDGSWDldDiHGC/bn1/oU2cCxIZu06QnYEOOOAUbCWdzgcT1F9fRjzzHbu0GxQ==", "license": "MIT-0", "dependencies": { "@aws-lambda-powertools/commons": "2.31.0", @@ -191,8 +188,6 @@ }, "node_modules/@aws-lambda-powertools/parameters": { "version": "2.31.0", - "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/parameters/-/parameters-2.31.0.tgz", - "integrity": "sha512-CO3JVcnBWTWaGwNslGadUfzr8eQiSiRUQCUp5IVdN7TULD6CNYV32VI35AkOFhGstmEC1BKP36T+AzVGxQWIew==", "license": "MIT-0", "dependencies": { "@aws-lambda-powertools/commons": "2.31.0" @@ -228,8 +223,6 @@ }, "node_modules/@aws-sdk/client-backup": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-backup/-/client-backup-3.988.0.tgz", - "integrity": "sha512-UZssTmSt1kQyJRr52w7GYnNcQsLOMZMDxSiTMSz5lmcUI9/PTUPWgTqBFacI04eKWlaYdSH/a8dQ2mu2fBrUHg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -278,8 +271,6 @@ }, "node_modules/@aws-sdk/client-backup/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -345,8 +336,6 @@ }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.988.0.tgz", - "integrity": "sha512-uL0ofMUufIFqGO4CBkQM16EbxUda59WGv/SMRfa2tiFolZ/3zaM5c+NWfsZazGHKaF/KjcNVs3rDT8jOTV+pjQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -396,8 +385,6 @@ }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -412,8 +399,6 @@ }, "node_modules/@aws-sdk/client-sqs": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.988.0.tgz", - "integrity": "sha512-Rwh+YhdhhGPFfZ0kLDl1GkLJ0xed0rgQnlGTHiVrwFE51nZm/Is6Car7m1nE4j0Rohom06qNClP3btt0mOf/pg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -464,8 +449,6 @@ }, "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -480,8 +463,6 @@ }, "node_modules/@aws-sdk/client-ssm": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.988.0.tgz", - "integrity": "sha512-Pm5ukPtUjf6R4hKgu/FPKDYR3fgEjXwMlK/q7Gtoxi8X9GETEwD8QTbo4AdMfjGFIbSbgVjHRvkOWPl+HuU8Gg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -531,8 +512,6 @@ }, "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -547,8 +526,6 @@ }, "node_modules/@aws-sdk/client-sso": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.988.0.tgz", - "integrity": "sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -596,8 +573,6 @@ }, "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -612,8 +587,6 @@ }, "node_modules/@aws-sdk/core": { "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.8.tgz", - "integrity": "sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -636,8 +609,6 @@ }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.6.tgz", - "integrity": "sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -652,8 +623,6 @@ }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.8.tgz", - "integrity": "sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -673,8 +642,6 @@ }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.6.tgz", - "integrity": "sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -698,8 +665,6 @@ }, "node_modules/@aws-sdk/credential-provider-login": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.6.tgz", - "integrity": "sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -717,8 +682,6 @@ }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.7.tgz", - "integrity": "sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.6", @@ -740,8 +703,6 @@ }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.6.tgz", - "integrity": "sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -757,8 +718,6 @@ }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.6.tgz", - "integrity": "sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-sso": "3.988.0", @@ -776,8 +735,6 @@ }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.6.tgz", - "integrity": "sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -794,8 +751,6 @@ }, "node_modules/@aws-sdk/dynamodb-codec": { "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.9.tgz", - "integrity": "sha512-ITv7JRqIgSRDbwmC3rUnQ7hoyRv+oH+ww0Gz+f2z8VTlS69STSLbfNiHGJrZ3TT4QEai9QYGwhSdjRYg1btglQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -894,8 +849,6 @@ }, "node_modules/@aws-sdk/middleware-sdk-sqs": { "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.7.tgz", - "integrity": "sha512-DcJLYE4sRjgUyb2SupQGaRgBYc+j89N9nXeMT0PwwVvaBGmKqcxa7PFvz0kBnQrBckPWlfrPyyyMwOeT5BEp6Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -911,8 +864,6 @@ }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.8.tgz", - "integrity": "sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -929,8 +880,6 @@ }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -945,8 +894,6 @@ }, "node_modules/@aws-sdk/nested-clients": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.988.0.tgz", - "integrity": "sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -994,8 +941,6 @@ }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -1024,8 +969,6 @@ }, "node_modules/@aws-sdk/token-providers": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.988.0.tgz", - "integrity": "sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -1111,8 +1054,6 @@ }, "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.6.tgz", - "integrity": "sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.8", @@ -1135,8 +1076,6 @@ }, "node_modules/@aws-sdk/xml-builder": { "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", - "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -1660,8 +1599,6 @@ }, "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "dev": true, "license": "MIT", "dependencies": { @@ -1670,15 +1607,11 @@ }, "node_modules/@emotion/memoize": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "dev": true, "license": "MIT" }, "node_modules/@emotion/unitless": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", "dev": true, "license": "MIT" }, @@ -1940,8 +1873,6 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -2282,8 +2213,6 @@ }, "node_modules/@faker-js/faker": { "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", - "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", "dev": true, "funding": [ { @@ -2339,8 +2268,6 @@ }, "node_modules/@humanwhocodes/momoa": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", - "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2943,8 +2870,6 @@ }, "node_modules/@mswjs/interceptors": { "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.2.tgz", - "integrity": "sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==", "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", @@ -2973,8 +2898,6 @@ }, "node_modules/@nhs/fhir-middy-error-handler": { "version": "2.1.72", - "resolved": "https://registry.npmjs.org/@nhs/fhir-middy-error-handler/-/fhir-middy-error-handler-2.1.72.tgz", - "integrity": "sha512-OgqzZA/+lWXYRa7aZsMwM0j84Aw2QGRRGY6V5N0mxS3pFNhQaJHs5HCDvAsa+WN8/PAM6PJTR+oxRW19hsc2qw==", "license": "MIT", "dependencies": { "@aws-lambda-powertools/logger": "^2.30.2", @@ -2983,8 +2906,6 @@ }, "node_modules/@noble/hashes": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", "engines": { @@ -2996,14 +2917,10 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", @@ -3012,8 +2929,6 @@ }, "node_modules/@open-draft/until": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "license": "MIT" }, "node_modules/@opentelemetry/api": { @@ -3312,8 +3227,6 @@ }, "node_modules/@redocly/cli": { "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.18.0.tgz", - "integrity": "sha512-LZCfASCqlHThihfXbs5OLR/ggBs+8GeMDeP6vw39eX4uyCOtSeadRGXnVlEbMqNOmXM7YITApPrWKiAoZ18uag==", "dev": true, "license": "MIT", "dependencies": { @@ -3550,8 +3463,6 @@ }, "node_modules/@redocly/config": { "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.43.0.tgz", - "integrity": "sha512-AbyFKRHKJ2VBmh9nO2lrG9tO2Gu/Lmnfdj4Uwoh7h/a7jWr1104t4fBgQZs/NwgGBAOkGmyQYAvardwyBeRGZA==", "dev": true, "license": "MIT", "dependencies": { @@ -3560,8 +3471,6 @@ }, "node_modules/@redocly/config/node_modules/json-schema-to-ts": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.7.2.tgz", - "integrity": "sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3575,15 +3484,11 @@ }, "node_modules/@redocly/config/node_modules/ts-algebra": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", - "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", "dev": true, "license": "MIT" }, "node_modules/@redocly/openapi-core": { "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.18.0.tgz", - "integrity": "sha512-I6SqmxPb/Q0Wf9eu3cbWxtTsHocgW/cL1+hEmS9C05aO1BQ+a+sd5BX96ffTxv8PRpwTeCdkuhmvmfczgvD3hA==", "dev": true, "license": "MIT", "dependencies": { @@ -3605,8 +3510,6 @@ }, "node_modules/@redocly/openapi-core/node_modules/@redocly/ajv": { "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.3.tgz", - "integrity": "sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng==", "dev": true, "license": "MIT", "dependencies": { @@ -3623,8 +3526,6 @@ "node_modules/@redocly/openapi-core/node_modules/ajv": { "name": "@redocly/ajv", "version": "8.17.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.2.tgz", - "integrity": "sha512-rcbDZOfXAgGEJeJ30aWCVVJvxV9ooevb/m1/SFblO2qHs4cqTk178gx7T/vdslf57EA4lTofrwsq5K8rxK9g+g==", "dev": true, "license": "MIT", "dependencies": { @@ -3640,15 +3541,11 @@ }, "node_modules/@redocly/openapi-core/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/@redocly/openapi-core/node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3660,15 +3557,11 @@ }, "node_modules/@redocly/openapi-core/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@redocly/openapi-core/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3680,8 +3573,6 @@ }, "node_modules/@redocly/respect-core": { "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-2.18.0.tgz", - "integrity": "sha512-mLCXnGYjiW+fq9AB3UAMhpOPAMd/ww2L2h+lbDOtTVC9PfFnC7QZcBKN40MoEwoM0b/KKf5y8lsSzkctLIj4XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3705,8 +3596,6 @@ }, "node_modules/@redocly/respect-core/node_modules/@faker-js/faker": { "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", "dev": true, "license": "MIT", "engines": { @@ -3717,8 +3606,6 @@ "node_modules/@redocly/respect-core/node_modules/ajv": { "name": "@redocly/ajv", "version": "8.17.1", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", "dev": true, "license": "MIT", "dependencies": { @@ -3734,22 +3621,16 @@ }, "node_modules/@redocly/respect-core/node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/@redocly/respect-core/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@redocly/respect-core/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3830,8 +3711,6 @@ }, "node_modules/@smithy/core": { "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.0.tgz", - "integrity": "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -3937,8 +3816,6 @@ }, "node_modules/@smithy/middleware-endpoint": { "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz", - "integrity": "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.0", @@ -3956,8 +3833,6 @@ }, "node_modules/@smithy/middleware-retry": { "version": "4.4.31", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz", - "integrity": "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", @@ -4012,8 +3887,6 @@ }, "node_modules/@smithy/node-http-handler": { "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", - "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", @@ -4111,8 +3984,6 @@ }, "node_modules/@smithy/smithy-client": { "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.3.tgz", - "integrity": "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.0", @@ -4204,8 +4075,6 @@ }, "node_modules/@smithy/util-defaults-mode-browser": { "version": "4.3.30", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz", - "integrity": "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", @@ -4219,8 +4088,6 @@ }, "node_modules/@smithy/util-defaults-mode-node": { "version": "4.2.33", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz", - "integrity": "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", @@ -4282,8 +4149,6 @@ }, "node_modules/@smithy/util-stream": { "version": "4.5.12", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", - "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", @@ -4462,8 +4327,6 @@ }, "node_modules/@types/node": { "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", "peer": true, @@ -4491,8 +4354,6 @@ }, "node_modules/@types/stylis": { "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", - "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", "dev": true, "license": "MIT" }, @@ -4517,8 +4378,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", - "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4546,8 +4405,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", - "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", "peer": true, @@ -4572,8 +4429,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", - "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4594,8 +4449,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", - "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4612,8 +4465,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", - "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", "dev": true, "license": "MIT", "engines": { @@ -4629,8 +4480,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", - "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", "dev": true, "license": "MIT", "dependencies": { @@ -4654,8 +4503,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", - "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", "dev": true, "license": "MIT", "engines": { @@ -4668,8 +4515,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", - "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", "dev": true, "license": "MIT", "dependencies": { @@ -4696,8 +4541,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", - "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", "dev": true, "license": "MIT", "dependencies": { @@ -4720,8 +4563,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", - "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", "dev": true, "license": "MIT", "dependencies": { @@ -4738,8 +4579,6 @@ }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5239,8 +5078,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -5301,8 +5138,6 @@ }, "node_modules/axios": { "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "peer": true, "dependencies": { @@ -5455,8 +5290,6 @@ }, "node_modules/better-ajv-errors": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz", - "integrity": "sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5852,8 +5685,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5935,8 +5766,6 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, @@ -6011,8 +5840,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6146,8 +5973,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6166,8 +5991,6 @@ }, "node_modules/esbuild": { "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -6607,8 +6430,6 @@ }, "node_modules/fast-xml-parser": { "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", @@ -6757,8 +6578,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7216,8 +7035,6 @@ }, "node_modules/is-node-process": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "license": "MIT" }, "node_modules/is-number": { @@ -8045,8 +7862,6 @@ }, "node_modules/jsonpath-rfc9535": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsonpath-rfc9535/-/jsonpath-rfc9535-1.3.0.tgz", - "integrity": "sha512-3jFHya7oZ45aDxIIdx+/zQARahHXxFSMWBkcBUldfXpLS9VCXDJyTKt35kQfEXLqh0K3Ixw/9xFnvcDStaxh7Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -8055,8 +7870,6 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, "license": "MIT", "engines": { @@ -8330,8 +8143,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8339,8 +8150,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -8571,8 +8380,6 @@ }, "node_modules/nock": { "version": "14.0.11", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.11.tgz", - "integrity": "sha512-u5xUnYE+UOOBA6SpELJheMCtj2Laqx15Vl70QxKo43Wz/6nMHXS7PrEioXLjXAwhmawdEMNImwKCcPhBJWbKVw==", "license": "MIT", "dependencies": { "@mswjs/interceptors": "^0.41.0", @@ -8670,6 +8477,10 @@ "node": ">=0.10.0" } }, + "node_modules/notificationsReportingLambda": { + "resolved": "packages/notificationsReportingLambda", + "link": true + }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", "dev": true, @@ -8787,8 +8598,6 @@ }, "node_modules/openapi-sampler": { "version": "1.6.2", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.2.tgz", - "integrity": "sha512-NyKGiFKfSWAZr4srD/5WDhInOWDhfml32h/FKUqLpEwKJt0kG0LGUU0MdyNkKrVGuJnw6DuPWq/sHCwAMpiRxg==", "dev": true, "license": "MIT", "dependencies": { @@ -8868,15 +8677,11 @@ }, "node_modules/outdent": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", - "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", "dev": true, "license": "MIT" }, "node_modules/outvariant": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "license": "MIT" }, "node_modules/p-limit": { @@ -9199,8 +9004,6 @@ }, "node_modules/prettier": { "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "peer": true, @@ -10065,8 +9868,6 @@ }, "node_modules/strict-event-emitter": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "license": "MIT" }, "node_modules/string_decoder": { @@ -10225,8 +10026,6 @@ }, "node_modules/strnum": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -10237,8 +10036,6 @@ }, "node_modules/styled-components": { "version": "6.3.9", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.9.tgz", - "integrity": "sha512-J72R4ltw0UBVUlEjTzI0gg2STOqlI9JBhQOL4Dxt7aJOnnSesy0qJDn4PYfMCafk9cWOaVg129Pesl5o+DIh0Q==", "dev": true, "license": "MIT", "peer": true, @@ -10272,8 +10069,6 @@ }, "node_modules/stylis": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "dev": true, "license": "MIT" }, @@ -10639,8 +10434,6 @@ }, "node_modules/typescript-eslint": { "version": "8.55.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", - "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", "dev": true, "license": "MIT", "dependencies": { @@ -11222,8 +11015,6 @@ }, "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/client-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", - "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -11276,8 +11067,6 @@ }, "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/lib-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", - "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -11296,8 +11085,6 @@ }, "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/util-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", - "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11311,8 +11098,6 @@ }, "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -11485,8 +11270,6 @@ }, "packages/gsul/node_modules/@aws-sdk/client-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", - "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -11539,8 +11322,6 @@ }, "packages/gsul/node_modules/@aws-sdk/lib-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", - "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -11559,8 +11340,6 @@ }, "packages/gsul/node_modules/@aws-sdk/util-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", - "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11574,8 +11353,6 @@ }, "packages/gsul/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -11637,6 +11414,23 @@ "axios-mock-adapter": "^2.1.0" } }, + "packages/notificationsReportingLambda": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-sdk/client-dynamodb": "^3.975.0", + "@aws-sdk/lib-dynamodb": "^3.975.0", + "@middy/core": "^7.0.2", + "@middy/http-header-normalizer": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@psu-common/commonTypes": "^1.0.0" + }, + "devDependencies": { + "@psu-common/testing": "^1.0.0" + } + }, "packages/postDatedLambda": { "version": "1.0.0", "license": "MIT", @@ -11695,8 +11489,6 @@ }, "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/client-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", - "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -11749,8 +11541,6 @@ }, "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/lib-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", - "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.8", @@ -11769,8 +11559,6 @@ }, "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/util-dynamodb": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", - "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11784,8 +11572,6 @@ }, "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/util-endpoints": { "version": "3.988.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", - "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", diff --git a/package.json b/package.json index 7f80f35152..1af0bf6757 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "packages/cpsuLambda", "packages/checkPrescriptionStatusUpdates", "packages/nhsNotifyLambda", + "packages/notificationsReportingLambda", "packages/postDatedLambda", "packages/psuRestoreValidationLambda", "packages/nhsNotifyUpdateCallback", diff --git a/packages/common/commonTypes/src/index.ts b/packages/common/commonTypes/src/index.ts index 808e28df60..d2495d5c39 100644 --- a/packages/common/commonTypes/src/index.ts +++ b/packages/common/commonTypes/src/index.ts @@ -49,6 +49,7 @@ export interface LastNotificationStateType extends NotificationUpdate { NHSNumber: string ODSCode: string RequestId: string // x-request-id header + PrescriptionID?: string SQSMessageID?: string // The SQS message ID NotifyMessageID?: string // The UUID we got back from Notify for the submitted message NotifyMessageReference: string // The references we generated for the message diff --git a/packages/nhsNotifyLambda/src/utils/dynamo.ts b/packages/nhsNotifyLambda/src/utils/dynamo.ts index aed6a2a352..ae3ed71e42 100644 --- a/packages/nhsNotifyLambda/src/utils/dynamo.ts +++ b/packages/nhsNotifyLambda/src/utils/dynamo.ts @@ -35,6 +35,7 @@ export async function addPrescriptionMessagesToNotificationStateStore( NHSNumber: data.PSUDataItem.PatientNHSNumber, ODSCode: data.PSUDataItem.PharmacyODSCode, RequestId: data.PSUDataItem.RequestID, + PrescriptionID: data.PSUDataItem.PrescriptionID, // only used for reporting and tracing SQSMessageID: data.MessageId, LastNotifiedPrescriptionStatus: data.PSUDataItem.Status, MessageStatus: data.messageStatus ?? "unknown", // Fall back to unknown if not set diff --git a/packages/notificationsReportingLambda/README.md b/packages/notificationsReportingLambda/README.md new file mode 100644 index 0000000000..be9d3480ff --- /dev/null +++ b/packages/notificationsReportingLambda/README.md @@ -0,0 +1,92 @@ +# Notifications reporting lambda + +This lambda exposes a lightweight, internal reporting endpoint for notification status records stored in the notifications DynamoDB table. + +## Overview + +- **API path**: `GET /notifications-reporting` +- **Purpose**: return notification status records for troubleshooting and test reporting. +- **Data source**: the notifications table (`PrescriptionNotificationStatesTableName`). +- **Deployment**: controlled by the `DeployNotificationsReporting` parameter. + +## Request + +Query parameters (at least one is required): + +- `prescriptionId` (string) +- `nhsNumber` (string) +- `odsCode` (string) + +Notes: + +- `odsCode` is upper‑cased before querying. +- If `nhsNumber` is provided, the lambda performs a **query** by partition key and applies additional filters. +- If only `prescriptionId` and/or `odsCode` are provided, the lambda performs a **scan** using those filters. + +### Example + +``` +GET /notifications-reporting?nhsNumber=9999999999&odsCode=A12345 +``` + +## Response + +Success (200): + +```json +{ + "count": 2, + "filters": { + "prescriptionId": null, + "odsCode": "A12345", + "nhsNumberProvided": true + }, + "notifications": [ + { + "NHSNumber": "9999999999", + "ODSCode": "A12345", + "RequestId": "req-1", + "PrescriptionID": "RX123", + "LastNotifiedPrescriptionStatus": "DELIVERED", + "LastNotificationRequestTimestamp": "2025-01-01T10:00:00.000Z", + "ExpiryTime": 1735735200, + "NotifyMessageReference": "ref-1", + "MessageStatus": "delivered", + "SupplierStatus": "delivered" + } + ] +} +``` + +Error responses: + +- **400** when no filters are provided. +- **500** for unexpected failures reading DynamoDB. + +## Data flow + +```mermaid +sequenceDiagram + autonumber + actor Client + participant APIGW as API Gateway + participant Lambda as NotificationsReporting Lambda + participant DynamoDB as Notifications Table + + Client->>APIGW: GET /notifications-reporting?filters + APIGW->>Lambda: Invoke (event) + Lambda->>Lambda: Normalize filters + alt No filters + Lambda-->>APIGW: 400 {message} + APIGW-->>Client: 400 + else Filters present + alt nhsNumber provided + Lambda->>DynamoDB: Query by NHSNumber + filters + else no nhsNumber + Lambda->>DynamoDB: Scan by filters + end + DynamoDB-->>Lambda: Items + Lambda-->>APIGW: 200 {count, filters, notifications} + APIGW-->>Client: 200 + end +``` diff --git a/packages/notificationsReportingLambda/jest.config.ts b/packages/notificationsReportingLambda/jest.config.ts new file mode 100644 index 0000000000..acbc1493de --- /dev/null +++ b/packages/notificationsReportingLambda/jest.config.ts @@ -0,0 +1,9 @@ +import defaultConfig from "../../jest.default.config.ts" +import type {JestConfigWithTsJest} from "ts-jest" + +const jestConfig: JestConfigWithTsJest = { + ...defaultConfig, + rootDir: "./" +} + +export default jestConfig diff --git a/packages/notificationsReportingLambda/package.json b/packages/notificationsReportingLambda/package.json new file mode 100644 index 0000000000..dea3cb4930 --- /dev/null +++ b/packages/notificationsReportingLambda/package.json @@ -0,0 +1,29 @@ +{ + "name": "notificationsReportingLambda", + "version": "1.0.0", + "description": "Lambda that exposes internal notification reporting APIs", + "main": "notificationsReportingLambda.js", + "author": "NHS Digital", + "license": "MIT", + "type": "module", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-sdk/client-dynamodb": "^3.975.0", + "@aws-sdk/lib-dynamodb": "^3.975.0", + "@middy/core": "^7.0.2", + "@middy/http-header-normalizer": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@psu-common/commonTypes": "^1.0.0" + }, + "devDependencies": { + "@psu-common/testing": "^1.0.0" + } +} diff --git a/packages/notificationsReportingLambda/src/notificationsReportingLambda.ts b/packages/notificationsReportingLambda/src/notificationsReportingLambda.ts new file mode 100644 index 0000000000..8de50d0185 --- /dev/null +++ b/packages/notificationsReportingLambda/src/notificationsReportingLambda.ts @@ -0,0 +1,100 @@ +import {APIGatewayProxyEvent, APIGatewayProxyResult} from "aws-lambda" +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import httpHeaderNormalizer from "@middy/http-header-normalizer" +import {DynamoDBClient} from "@aws-sdk/client-dynamodb" +import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" + +import {NotificationQueryFilters, NotificationsRepository} from "./notificationsRepository" + +const logger = new Logger({serviceName: "notificationsReporting"}) + +const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({region: process.env.AWS_REGION})) +let cachedRepository: NotificationsRepository | undefined + +const getDefaultRepository = (): NotificationsRepository => { + cachedRepository ??= new NotificationsRepository(documentClient, process.env.TABLE_NAME ?? "") + return cachedRepository +} + +export const buildHandler = (repoProvider: () => NotificationsRepository) => { + return async (event: APIGatewayProxyEvent): Promise => { + logger.appendKeys({ + "nhsd-correlation-id": event.headers["nhsd-correlation-id"], + "x-request-id": event.headers["x-request-id"], + "apigw-request-id": event.requestContext.requestId + }) + + const filters = normalizeFilters(event.queryStringParameters ?? {}) + + if (!hasAnyFilter(filters)) { + return buildResponse(400, {message: "Provide at least one of prescriptionId, nhsNumber or odsCode"}) + } + + try { + const notifications = await repoProvider().fetch(filters) + logger.info("Returning notification results", { + hasNhsNumber: Boolean(filters.nhsNumber), + odsCode: filters.odsCode, + prescriptionIdProvided: Boolean(filters.prescriptionId), + resultCount: notifications.length + }) + return buildResponse(200, { + count: notifications.length, + filters: { + prescriptionId: filters.prescriptionId ?? null, + odsCode: filters.odsCode ?? null, + nhsNumberProvided: Boolean(filters.nhsNumber) + }, + notifications + }) + } catch (error) { + logger.error("Failed to retrieve notification records", {error}) + return buildResponse(500, {message: "Failed to fetch notification statuses"}) + } + } +} + +const normalizeFilters = (params: Record): NotificationQueryFilters => { + const trimmed = (value?: string | null) => { + if (!value) return undefined + const normalized = value.trim() + return normalized.length ? normalized : undefined + } + + return { + prescriptionId: trimmed(params.prescriptionId), + nhsNumber: trimmed(params.nhsNumber), + odsCode: trimmed(params.odsCode)?.toUpperCase() + } +} + +const hasAnyFilter = (filters: NotificationQueryFilters): boolean => ( + Boolean(filters.prescriptionId) || Boolean(filters.nhsNumber) || Boolean(filters.odsCode) +) + +const buildResponse = (statusCode: number, body: unknown): APIGatewayProxyResult => ({ + statusCode, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache" + }, + body: JSON.stringify(body) +}) + +export const lambdaHandler = buildHandler(getDefaultRepository) + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use(httpHeaderNormalizer()) + .use( + inputOutputLogger({ + logger: (request) => { + logger.info(request) + } + }) + ) + +export {normalizeFilters, buildResponse} diff --git a/packages/notificationsReportingLambda/src/notificationsRepository.ts b/packages/notificationsReportingLambda/src/notificationsRepository.ts new file mode 100644 index 0000000000..e4420b1bf3 --- /dev/null +++ b/packages/notificationsReportingLambda/src/notificationsRepository.ts @@ -0,0 +1,156 @@ +import { + DynamoDBDocumentClient, + QueryCommand, + ScanCommand, + QueryCommandInput, + ScanCommandInput +} from "@aws-sdk/lib-dynamodb" +import {LastNotificationStateType} from "@psu-common/commonTypes" + +export interface NotificationQueryFilters { + prescriptionId?: string + nhsNumber?: string + odsCode?: string +} + +interface FilterState { + clauses: Array + names: Record + values: Record + index: number +} + +export class NotificationsRepository { + private readonly client: DynamoDBDocumentClient + private readonly tableName: string + + constructor(client: DynamoDBDocumentClient, tableName: string) { + if (!tableName) { + throw new Error("Notifications table name is not configured") + } + this.client = client + this.tableName = tableName + } + + async fetch(filters: NotificationQueryFilters): Promise> { + if (!filters.nhsNumber && !filters.prescriptionId && !filters.odsCode) { + throw new Error("At least one filter must be provided") + } + + if (filters.nhsNumber) { + return await this.queryByNhsNumber(filters) + } + + return await this.scanByFilters(filters) + } + + // We can query by NHS number, since it's a partition key. Other fields will require a scan + private async queryByNhsNumber(filters: NotificationQueryFilters): Promise> { + const names: Record = {"#pk": "NHSNumber"} + const values: Record = {":pk": filters.nhsNumber} + const filterState: FilterState = { + clauses: [], + names, + values, + index: 0 + } + + if (filters.odsCode) { + this.appendFilterClause(filterState, "ODSCode", filters.odsCode) + } + if (filters.prescriptionId) { + this.appendFilterClause(filterState, "PrescriptionID", filters.prescriptionId) + } + + const input: QueryCommandInput = { + TableName: this.tableName, + KeyConditionExpression: "#pk = :pk", + ExpressionAttributeNames: names, + ExpressionAttributeValues: values + } + + if (filterState.clauses.length) { + input.FilterExpression = filterState.clauses.join(" AND ") + } + + return await this.paginatedQuery(input) + } + + private async scanByFilters(filters: NotificationQueryFilters): Promise> { + const names: Record = {} + const values: Record = {} + const filterState: FilterState = { + clauses: [], + names, + values, + index: 0 + } + + if (filters.prescriptionId) { + this.appendFilterClause(filterState, "PrescriptionID", filters.prescriptionId) + } + if (filters.odsCode) { + this.appendFilterClause(filterState, "ODSCode", filters.odsCode) + } + + if (filterState.clauses.length === 0) { + throw new Error("Scan requires at least one non-key filter") + } + + const input: ScanCommandInput = { + TableName: this.tableName, + FilterExpression: filterState.clauses.join(" AND "), + ExpressionAttributeNames: names, + ExpressionAttributeValues: values + } + + return await this.paginatedScan(input) + } + + private appendFilterClause(state: FilterState, attributeName: string, value: string): void { + const nameKey = `#f${state.index}` + const valueKey = `:f${state.index}` + state.index += 1 + state.names[nameKey] = attributeName + state.values[valueKey] = value + state.clauses.push(`${nameKey} = ${valueKey}`) + } + + private async paginatedQuery(input: QueryCommandInput): Promise> { + const items: Array = [] + let exclusiveStartKey: Record | undefined + + do { + const command = new QueryCommand({ + ...input, + ExclusiveStartKey: exclusiveStartKey + }) + const result = await this.client.send(command) + if (result.Items) { + items.push(...result.Items as Array) + } + exclusiveStartKey = result.LastEvaluatedKey as Record | undefined + } while (exclusiveStartKey) + + return items + } + + private async paginatedScan(input: ScanCommandInput): Promise> { + const items: Array = [] + let exclusiveStartKey: Record | undefined + + do { + const command = new ScanCommand({ + ...input, + ExclusiveStartKey: exclusiveStartKey + }) + const result = await this.client.send(command) + if (result.Items) { + items.push(...result.Items as Array) + } + exclusiveStartKey = result.LastEvaluatedKey as Record | undefined + } while (exclusiveStartKey) + + return items + } +} diff --git a/packages/notificationsReportingLambda/tests/notificationsReportingLambda.test.ts b/packages/notificationsReportingLambda/tests/notificationsReportingLambda.test.ts new file mode 100644 index 0000000000..597b34a897 --- /dev/null +++ b/packages/notificationsReportingLambda/tests/notificationsReportingLambda.test.ts @@ -0,0 +1,78 @@ +import { + jest, + describe, + it, + expect, + beforeEach +} from "@jest/globals" +import {APIGatewayProxyResult} from "aws-lambda" + +import {buildHandler} from "../src/notificationsReportingLambda" +import type {NotificationsRepository} from "../src/notificationsRepository" +import {mockAPIGatewayProxyEvent} from "@psu-common/testing" + +const buildEvent = (query: Record) => ({ + ...mockAPIGatewayProxyEvent, + httpMethod: "GET", + queryStringParameters: query +}) + +describe("notificationsReportingLambda", () => { + let fetchMock: jest.MockedFunction + let repo: NotificationsRepository + + beforeEach(() => { + fetchMock = jest.fn() as jest.MockedFunction + repo = {fetch: fetchMock} as unknown as NotificationsRepository + }) + + it("returns 400 when no filters are provided", async () => { + const handler = buildHandler(() => repo) + const result: APIGatewayProxyResult = await handler(buildEvent({})) + + expect(result.statusCode).toBe(400) + expect(JSON.parse(result.body)).toEqual({ + message: "Provide at least one of prescriptionId, nhsNumber or odsCode" + }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it("delegates to the repository and returns matches", async () => { + fetchMock.mockResolvedValue([ + { + RequestId: "req-1", + NHSNumber: "123", + ODSCode: "A1B", + LastNotifiedPrescriptionStatus: "DELIVERED", + LastNotificationRequestTimestamp: new Date().toISOString(), + ExpiryTime: 123, + NotifyMessageReference: "ref-1" + } + ]) + + const handler = buildHandler(() => repo) + const result: APIGatewayProxyResult = await handler( + buildEvent({nhsNumber: " 123 ", odsCode: "ab1"}) + ) + + expect(fetchMock).toHaveBeenCalledWith({nhsNumber: "123", odsCode: "AB1"}) + expect(result.statusCode).toBe(200) + expect(JSON.parse(result.body)).toMatchObject({ + count: 1, + filters: { + nhsNumberProvided: true, + odsCode: "AB1" + } + }) + }) + + it("handles repository errors", async () => { + fetchMock.mockRejectedValue(new Error("boom")) + const handler = buildHandler(() => repo) + + const result = await handler(buildEvent({prescriptionId: "rx-1"})) + + expect(result.statusCode).toBe(500) + expect(JSON.parse(result.body)).toEqual({message: "Failed to fetch notification statuses"}) + }) +}) diff --git a/packages/notificationsReportingLambda/tests/notificationsRepository.test.ts b/packages/notificationsReportingLambda/tests/notificationsRepository.test.ts new file mode 100644 index 0000000000..5ad03669e2 --- /dev/null +++ b/packages/notificationsReportingLambda/tests/notificationsRepository.test.ts @@ -0,0 +1,104 @@ +import { + jest, + describe, + it, + expect, + beforeEach +} from "@jest/globals" +import {QueryCommand, ScanCommand} from "@aws-sdk/lib-dynamodb" +import type {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" + +import {NotificationsRepository} from "../src/notificationsRepository" + +const mockSend: jest.MockedFunction<(command: QueryCommand | ScanCommand) => Promise> = jest.fn() +const mockClient = {send: mockSend} as unknown as DynamoDBDocumentClient + +describe("NotificationsRepository", () => { + beforeEach(() => { + mockSend.mockReset() + }) + + it("queries by NHS number when provided", async () => { + mockSend.mockResolvedValue({ + Items: [{ + NHSNumber: "123", + RequestId: "req", + ODSCode: "F1A", + LastNotificationRequestTimestamp: new Date().toISOString(), + ExpiryTime: 123, + LastNotifiedPrescriptionStatus: "DELIVERED", + NotifyMessageReference: "ref" + }] + }) + + const repo = new NotificationsRepository(mockClient, "table") + const results = await repo.fetch({nhsNumber: "123", odsCode: "F1A"}) + + expect(results).toHaveLength(1) + expect(mockSend).toHaveBeenCalledTimes(1) + const command = mockSend.mock.calls[0][0] as QueryCommand + expect(command).toBeInstanceOf(QueryCommand) + expect(command.input.ExpressionAttributeNames?.["#pk"]).toBe("NHSNumber") + expect(command.input.FilterExpression).toBe("#f0 = :f0") + expect(command.input.ExpressionAttributeNames?.["#f0"]).toBe("ODSCode") + }) + + it("scans when only non-key filters are supplied", async () => { + mockSend + .mockResolvedValueOnce({ + Items: [{ + PrescriptionID: "RX1", + RequestId: "req-1", + NHSNumber: "999", + ODSCode: "T1O", + LastNotificationRequestTimestamp: new Date().toISOString(), + ExpiryTime: 123, + LastNotifiedPrescriptionStatus: "REQUESTED", + NotifyMessageReference: "ref-1" + }], + LastEvaluatedKey: {RequestId: "req-1"} + }) + .mockResolvedValueOnce({ + Items: [{ + PrescriptionID: "RX1", + RequestId: "req-2", + NHSNumber: "999", + ODSCode: "T1O", + LastNotificationRequestTimestamp: new Date().toISOString(), + ExpiryTime: 456, + LastNotifiedPrescriptionStatus: "REQUESTED", + NotifyMessageReference: "ref-2" + }] + }) + + const repo = new NotificationsRepository(mockClient, "table") + const results = await repo.fetch({prescriptionId: "rx1", odsCode: "t1o"}) + + expect(results).toHaveLength(2) + expect(mockSend).toHaveBeenCalledTimes(2) + const firstCall = mockSend.mock.calls[0][0] as ScanCommand + expect(firstCall).toBeInstanceOf(ScanCommand) + expect(firstCall.input.FilterExpression).toBe("#f0 = :f0 AND #f1 = :f1") + expect(firstCall.input.ExpressionAttributeNames?.["#f0"]).toBe("PrescriptionID") + expect(firstCall.input.ExpressionAttributeNames?.["#f1"]).toBe("ODSCode") + }) + + it("allows scanning by ODS code only", async () => { + mockSend.mockResolvedValue({Items: []}) + + const repo = new NotificationsRepository(mockClient, "table") + await repo.fetch({odsCode: "A1B"}) + + const call = mockSend.mock.calls[0][0] as ScanCommand + expect(call).toBeInstanceOf(ScanCommand) + expect(call.input.FilterExpression).toBe("#f0 = :f0") + expect(call.input.ExpressionAttributeNames?.["#f0"]).toBe("ODSCode") + }) + + it("throws when no filters are supplied", async () => { + const repo = new NotificationsRepository(mockClient, "table") + + await expect(repo.fetch({})).rejects.toThrow("At least one filter must be provided") + }) + +}) diff --git a/packages/notificationsReportingLambda/tsconfig.json b/packages/notificationsReportingLambda/tsconfig.json new file mode 100644 index 0000000000..eac36c1c82 --- /dev/null +++ b/packages/notificationsReportingLambda/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib" + }, + "references": [ + { + "path": "../common/testing" + } + ], + "include": [ + "src/**/*", + "tests/**/*", + "cli/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/sonar-project.properties b/sonar-project.properties index 815621e84a..afda66e577 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -26,6 +26,7 @@ sonar.modules=\ cpsuLambda, \ gsul, \ nhsNotifyLambda, \ + notificationsReportingLambda, \ nhsNotifyUpdateCallback, \ psuRestoreValidationLambda, \ sandbox, \ @@ -53,6 +54,10 @@ nhsNotifyLambda.sonar.projectBaseDir=packages/nhsNotifyLambda nhsNotifyLambda.sonar.sources=. nhsNotifyLambda.sonar.javascript.lcov.reportPaths=coverage/lcov.info +notificationsReportingLambda.sonar.projectBaseDir=packages/notificationsReportingLambda +notificationsReportingLambda.sonar.sources=. +notificationsReportingLambda.sonar.javascript.lcov.reportPaths=coverage/lcov.info + postDatedLambda.sonar.projectBaseDir=packages/postDatedLambda postDatedLambda.sonar.sources=. postDatedLambda.sonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/tsconfig.build.json b/tsconfig.build.json index 479e9c0313..47cb9c8044 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -45,6 +45,9 @@ }, { "path": "packages/nhsNotifyUpdateCallback" + }, + { + "path": "packages/notificationsReportingLambda" } ] }