diff --git a/.github/workflows/ci-typescript.yml b/.github/workflows/ci-typescript.yml index 885339daa0..f5f78221ca 100644 --- a/.github/workflows/ci-typescript.yml +++ b/.github/workflows/ci-typescript.yml @@ -46,6 +46,7 @@ jobs: - generate-product-catalog - alarms-handler - discount-api + - discount-expiry-notifier - salesforce-disaster-recovery - salesforce-disaster-recovery-health-check - zuora-salesforce-link-remover diff --git a/cdk/bin/cdk.ts b/cdk/bin/cdk.ts index a9643eeb46..cb950da95d 100644 --- a/cdk/bin/cdk.ts +++ b/cdk/bin/cdk.ts @@ -4,6 +4,7 @@ import { AlarmsHandler } from '../lib/alarms-handler'; import { BatchEmailSender } from '../lib/batch-email-sender'; import { CancellationSfCasesApi } from '../lib/cancellation-sf-cases-api'; import { DiscountApi } from '../lib/discount-api'; +import { DiscountExpiryNotifier } from '../lib/discount-expiry-notifier'; import { GenerateProductCatalog } from '../lib/generate-product-catalog'; import type { NewProductApiProps } from '../lib/new-product-api'; import { NewProductApi } from '../lib/new-product-api'; @@ -287,3 +288,11 @@ new UserBenefits(app, 'user-benefits-PROD', { supporterProductDataTable: 'supporter-product-data-tables-PROD-SupporterProductDataTable', }); +new DiscountExpiryNotifier(app, 'discount-expiry-notifier-CODE', { + stack: 'support', + stage: 'CODE', +}); +new DiscountExpiryNotifier(app, 'discount-expiry-notifier-PROD', { + stack: 'support', + stage: 'PROD', +}); diff --git a/cdk/lib/__snapshots__/discount-expiry-notifier.test.ts.snap b/cdk/lib/__snapshots__/discount-expiry-notifier.test.ts.snap new file mode 100644 index 0000000000..79c716bb5b --- /dev/null +++ b/cdk/lib/__snapshots__/discount-expiry-notifier.test.ts.snap @@ -0,0 +1,965 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`The discount-expiry-notifier stack matches the snapshot 1`] = ` +{ + "Mappings": { + "ServiceprincipalMap": { + "af-south-1": { + "states": "states.af-south-1.amazonaws.com", + }, + "ap-east-1": { + "states": "states.ap-east-1.amazonaws.com", + }, + "ap-northeast-1": { + "states": "states.ap-northeast-1.amazonaws.com", + }, + "ap-northeast-2": { + "states": "states.ap-northeast-2.amazonaws.com", + }, + "ap-northeast-3": { + "states": "states.ap-northeast-3.amazonaws.com", + }, + "ap-south-1": { + "states": "states.ap-south-1.amazonaws.com", + }, + "ap-south-2": { + "states": "states.ap-south-2.amazonaws.com", + }, + "ap-southeast-1": { + "states": "states.ap-southeast-1.amazonaws.com", + }, + "ap-southeast-2": { + "states": "states.ap-southeast-2.amazonaws.com", + }, + "ap-southeast-3": { + "states": "states.ap-southeast-3.amazonaws.com", + }, + "ap-southeast-4": { + "states": "states.ap-southeast-4.amazonaws.com", + }, + "ca-central-1": { + "states": "states.ca-central-1.amazonaws.com", + }, + "cn-north-1": { + "states": "states.cn-north-1.amazonaws.com", + }, + "cn-northwest-1": { + "states": "states.cn-northwest-1.amazonaws.com", + }, + "eu-central-1": { + "states": "states.eu-central-1.amazonaws.com", + }, + "eu-central-2": { + "states": "states.eu-central-2.amazonaws.com", + }, + "eu-north-1": { + "states": "states.eu-north-1.amazonaws.com", + }, + "eu-south-1": { + "states": "states.eu-south-1.amazonaws.com", + }, + "eu-south-2": { + "states": "states.eu-south-2.amazonaws.com", + }, + "eu-west-1": { + "states": "states.eu-west-1.amazonaws.com", + }, + "eu-west-2": { + "states": "states.eu-west-2.amazonaws.com", + }, + "eu-west-3": { + "states": "states.eu-west-3.amazonaws.com", + }, + "il-central-1": { + "states": "states.il-central-1.amazonaws.com", + }, + "me-central-1": { + "states": "states.me-central-1.amazonaws.com", + }, + "me-south-1": { + "states": "states.me-south-1.amazonaws.com", + }, + "sa-east-1": { + "states": "states.sa-east-1.amazonaws.com", + }, + "us-east-1": { + "states": "states.us-east-1.amazonaws.com", + }, + "us-east-2": { + "states": "states.us-east-2.amazonaws.com", + }, + "us-gov-east-1": { + "states": "states.us-gov-east-1.amazonaws.com", + }, + "us-gov-west-1": { + "states": "states.us-gov-west-1.amazonaws.com", + }, + "us-iso-east-1": { + "states": "states.amazonaws.com", + }, + "us-iso-west-1": { + "states": "states.amazonaws.com", + }, + "us-isob-east-1": { + "states": "states.amazonaws.com", + }, + "us-west-1": { + "states": "states.us-west-1.amazonaws.com", + }, + "us-west-2": { + "states": "states.us-west-2.amazonaws.com", + }, + }, + }, + "Metadata": { + "gu:cdk:constructs": [ + "GuDistributionBucketParameter", + "GuLambdaFunction", + ], + "gu:cdk:version": "TEST", + }, + "Parameters": { + "DistributionBucketName": { + "Default": "/account/services/artifact.bucket", + "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "discountexpirynotifierstatemachineCODE793C8056": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "discountexpirynotifierstatemachineCODERoleDefaultPolicy1B7EA3A1", + "discountexpirynotifierstatemachineCODERole18E0A204", + ], + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{"StartAt":"Get Subs With Expiring Discounts","States":{"Get Subs With Expiring Discounts":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + "","Payload.$":"$"}}}}", + ], + ], + }, + "RoleArn": { + "Fn::GetAtt": [ + "discountexpirynotifierstatemachineCODERole18E0A204", + "Arn", + ], + }, + "StateMachineName": "discount-expiry-notifier-CODE", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::StepFunctions::StateMachine", + "UpdateReplacePolicy": "Delete", + }, + "discountexpirynotifierstatemachineCODERole18E0A204": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::FindInMap": [ + "ServiceprincipalMap", + { + "Ref": "AWS::Region", + }, + "states", + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "discountexpirynotifierstatemachineCODERoleDefaultPolicy1B7EA3A1": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "discountexpirynotifierstatemachineCODERoleDefaultPolicy1B7EA3A1", + "Roles": [ + { + "Ref": "discountexpirynotifierstatemachineCODERole18E0A204", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "getsubswithexpiringdiscountslambda78A8A9ED": { + "DependsOn": [ + "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA", + "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Ref": "DistributionBucketName", + }, + "S3Key": "membership/CODE/discount-expiry-notifier/discount-expiry-notifier.zip", + }, + "Environment": { + "Variables": { + "APP": "discount-expiry-notifier", + "STACK": "membership", + "STAGE": "CODE", + "Stage": "CODE", + }, + }, + "FunctionName": "discount-expiry-notifier-get-subs-with-expiring-discounts-CODE", + "Handler": "getSubsWithExpiringDiscounts.handler", + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Tags": [ + { + "Key": "App", + "Value": "discount-expiry-notifier", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "getsubswithexpiringdiscountslambdaServiceRole8D362B94": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "discount-expiry-notifier", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::", + { + "Ref": "DistributionBucketName", + }, + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::", + { + "Ref": "DistributionBucketName", + }, + "/membership/CODE/discount-expiry-notifier/discount-expiry-notifier.zip", + ], + ], + }, + ], + }, + { + "Action": "ssm:GetParametersByPath", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/membership/discount-expiry-notifier", + ], + ], + }, + }, + { + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/membership/discount-expiry-notifier/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA", + "Roles": [ + { + "Ref": "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; + +exports[`The discount-expiry-notifier stack matches the snapshot 2`] = ` +{ + "Mappings": { + "ServiceprincipalMap": { + "af-south-1": { + "states": "states.af-south-1.amazonaws.com", + }, + "ap-east-1": { + "states": "states.ap-east-1.amazonaws.com", + }, + "ap-northeast-1": { + "states": "states.ap-northeast-1.amazonaws.com", + }, + "ap-northeast-2": { + "states": "states.ap-northeast-2.amazonaws.com", + }, + "ap-northeast-3": { + "states": "states.ap-northeast-3.amazonaws.com", + }, + "ap-south-1": { + "states": "states.ap-south-1.amazonaws.com", + }, + "ap-south-2": { + "states": "states.ap-south-2.amazonaws.com", + }, + "ap-southeast-1": { + "states": "states.ap-southeast-1.amazonaws.com", + }, + "ap-southeast-2": { + "states": "states.ap-southeast-2.amazonaws.com", + }, + "ap-southeast-3": { + "states": "states.ap-southeast-3.amazonaws.com", + }, + "ap-southeast-4": { + "states": "states.ap-southeast-4.amazonaws.com", + }, + "ca-central-1": { + "states": "states.ca-central-1.amazonaws.com", + }, + "cn-north-1": { + "states": "states.cn-north-1.amazonaws.com", + }, + "cn-northwest-1": { + "states": "states.cn-northwest-1.amazonaws.com", + }, + "eu-central-1": { + "states": "states.eu-central-1.amazonaws.com", + }, + "eu-central-2": { + "states": "states.eu-central-2.amazonaws.com", + }, + "eu-north-1": { + "states": "states.eu-north-1.amazonaws.com", + }, + "eu-south-1": { + "states": "states.eu-south-1.amazonaws.com", + }, + "eu-south-2": { + "states": "states.eu-south-2.amazonaws.com", + }, + "eu-west-1": { + "states": "states.eu-west-1.amazonaws.com", + }, + "eu-west-2": { + "states": "states.eu-west-2.amazonaws.com", + }, + "eu-west-3": { + "states": "states.eu-west-3.amazonaws.com", + }, + "il-central-1": { + "states": "states.il-central-1.amazonaws.com", + }, + "me-central-1": { + "states": "states.me-central-1.amazonaws.com", + }, + "me-south-1": { + "states": "states.me-south-1.amazonaws.com", + }, + "sa-east-1": { + "states": "states.sa-east-1.amazonaws.com", + }, + "us-east-1": { + "states": "states.us-east-1.amazonaws.com", + }, + "us-east-2": { + "states": "states.us-east-2.amazonaws.com", + }, + "us-gov-east-1": { + "states": "states.us-gov-east-1.amazonaws.com", + }, + "us-gov-west-1": { + "states": "states.us-gov-west-1.amazonaws.com", + }, + "us-iso-east-1": { + "states": "states.amazonaws.com", + }, + "us-iso-west-1": { + "states": "states.amazonaws.com", + }, + "us-isob-east-1": { + "states": "states.amazonaws.com", + }, + "us-west-1": { + "states": "states.us-west-1.amazonaws.com", + }, + "us-west-2": { + "states": "states.us-west-2.amazonaws.com", + }, + }, + }, + "Metadata": { + "gu:cdk:constructs": [ + "GuDistributionBucketParameter", + "GuLambdaFunction", + ], + "gu:cdk:version": "TEST", + }, + "Parameters": { + "DistributionBucketName": { + "Default": "/account/services/artifact.bucket", + "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "discountexpirynotifierstatemachinePROD71101E82": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "discountexpirynotifierstatemachinePRODRoleDefaultPolicy61E4D4D9", + "discountexpirynotifierstatemachinePRODRole2A258F7B", + ], + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{"StartAt":"Get Subs With Expiring Discounts","States":{"Get Subs With Expiring Discounts":{"End":true,"Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","OutputPath":"$.Payload","Resource":"arn:", + { + "Ref": "AWS::Partition", + }, + ":states:::lambda:invoke","Parameters":{"FunctionName":"", + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + "","Payload.$":"$"}}}}", + ], + ], + }, + "RoleArn": { + "Fn::GetAtt": [ + "discountexpirynotifierstatemachinePRODRole2A258F7B", + "Arn", + ], + }, + "StateMachineName": "discount-expiry-notifier-PROD", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + "Type": "AWS::StepFunctions::StateMachine", + "UpdateReplacePolicy": "Delete", + }, + "discountexpirynotifierstatemachinePRODRole2A258F7B": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::FindInMap": [ + "ServiceprincipalMap", + { + "Ref": "AWS::Region", + }, + "states", + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "discountexpirynotifierstatemachinePRODRoleDefaultPolicy61E4D4D9": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambda78A8A9ED", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "discountexpirynotifierstatemachinePRODRoleDefaultPolicy61E4D4D9", + "Roles": [ + { + "Ref": "discountexpirynotifierstatemachinePRODRole2A258F7B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "getsubswithexpiringdiscountslambda78A8A9ED": { + "DependsOn": [ + "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA", + "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Ref": "DistributionBucketName", + }, + "S3Key": "membership/PROD/discount-expiry-notifier/discount-expiry-notifier.zip", + }, + "Environment": { + "Variables": { + "APP": "discount-expiry-notifier", + "STACK": "membership", + "STAGE": "PROD", + "Stage": "PROD", + }, + }, + "FunctionName": "discount-expiry-notifier-get-subs-with-expiring-discounts-PROD", + "Handler": "getSubsWithExpiringDiscounts.handler", + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Tags": [ + { + "Key": "App", + "Value": "discount-expiry-notifier", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "getsubswithexpiringdiscountslambdaServiceRole8D362B94": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "discount-expiry-notifier", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/support-service-lambdas", + }, + { + "Key": "Stack", + "Value": "membership", + }, + { + "Key": "Stage", + "Value": "PROD", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::", + { + "Ref": "DistributionBucketName", + }, + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::", + { + "Ref": "DistributionBucketName", + }, + "/membership/PROD/discount-expiry-notifier/discount-expiry-notifier.zip", + ], + ], + }, + ], + }, + { + "Action": "ssm:GetParametersByPath", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":parameter/PROD/membership/discount-expiry-notifier", + ], + ], + }, + }, + { + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":parameter/PROD/membership/discount-expiry-notifier/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "getsubswithexpiringdiscountslambdaServiceRoleDefaultPolicyD314C5BA", + "Roles": [ + { + "Ref": "getsubswithexpiringdiscountslambdaServiceRole8D362B94", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/cdk/lib/discount-expiry-notifier.test.ts b/cdk/lib/discount-expiry-notifier.test.ts new file mode 100644 index 0000000000..2dcf4e32da --- /dev/null +++ b/cdk/lib/discount-expiry-notifier.test.ts @@ -0,0 +1,28 @@ +import { App } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { DiscountExpiryNotifier } from './discount-expiry-notifier'; + +describe('The discount-expiry-notifier stack', () => { + it('matches the snapshot', () => { + const app = new App(); + const codeStack = new DiscountExpiryNotifier( + app, + 'discount-expiry-notifier-CODE', + { + stack: 'membership', + stage: 'CODE', + }, + ); + const prodStack = new DiscountExpiryNotifier( + app, + 'discount-expiry-notifier-PROD', + { + stack: 'membership', + stage: 'PROD', + }, + ); + + expect(Template.fromStack(codeStack).toJSON()).toMatchSnapshot(); + expect(Template.fromStack(prodStack).toJSON()).toMatchSnapshot(); + }); +}); diff --git a/cdk/lib/discount-expiry-notifier.ts b/cdk/lib/discount-expiry-notifier.ts new file mode 100644 index 0000000000..f67d3d99a8 --- /dev/null +++ b/cdk/lib/discount-expiry-notifier.ts @@ -0,0 +1,50 @@ +import type { GuStackProps } from '@guardian/cdk/lib/constructs/core'; +import { GuStack } from '@guardian/cdk/lib/constructs/core'; +import { GuLambdaFunction } from '@guardian/cdk/lib/constructs/lambda'; +import type { App } from 'aws-cdk-lib'; +import { Architecture } from 'aws-cdk-lib/aws-lambda'; +import { DefinitionBody, StateMachine } from 'aws-cdk-lib/aws-stepfunctions'; +import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; +import { nodeVersion } from './node-version'; + +export class DiscountExpiryNotifier extends GuStack { + constructor(scope: App, id: string, props: GuStackProps) { + super(scope, id, props); + + const appName = 'discount-expiry-notifier'; + + const getSubsWithExpiringDiscountsLambda = new GuLambdaFunction( + this, + 'get-subs-with-expiring-discounts-lambda', + { + app: appName, + functionName: `${appName}-get-subs-with-expiring-discounts-${this.stage}`, + runtime: nodeVersion, + environment: { + Stage: this.stage, + }, + handler: 'getSubsWithExpiringDiscounts.handler', + fileName: `${appName}.zip`, + architecture: Architecture.ARM_64, + }, + ); + + const getSubsWithExpiringDiscountsLambdaTask = new LambdaInvoke( + this, + 'Get Subs With Expiring Discounts', + { + lambdaFunction: getSubsWithExpiringDiscountsLambda, + outputPath: '$.Payload', + }, + ); + + const definitionBody = DefinitionBody.fromChainable( + getSubsWithExpiringDiscountsLambdaTask, + ); + + new StateMachine(this, `${appName}-state-machine-${this.stage}`, { + stateMachineName: `${appName}-${this.stage}`, + definitionBody: definitionBody, + }); + } +} diff --git a/handlers/discount-expiry-notifier/jest.config.js b/handlers/discount-expiry-notifier/jest.config.js new file mode 100644 index 0000000000..7370f0a864 --- /dev/null +++ b/handlers/discount-expiry-notifier/jest.config.js @@ -0,0 +1,10 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + runner: 'groups', + moduleNameMapper: { + '@modules/(.*)/(.*)$': '/../../modules/$1/src/$2', + '@modules/(.*)$': '/../../modules/$1', + }, +}; diff --git a/handlers/discount-expiry-notifier/package.json b/handlers/discount-expiry-notifier/package.json new file mode 100644 index 0000000000..c4280a7ca4 --- /dev/null +++ b/handlers/discount-expiry-notifier/package.json @@ -0,0 +1,19 @@ +{ + "name": "discount-expiry-notifier", + "description": "A state machine that initiates communications to customers whose discounts are about to end.", + "scripts": { + "test": "jest --group=-integration", + "it-test": "jest --group=integration", + "build": "esbuild --bundle --platform=node --target=node20 --outdir=target src/handlers/*.ts", + "lint": "eslint src/**/*.ts test/**/*.ts", + "package": "pnpm type-check && pnpm lint && pnpm check-formatting && pnpm test && pnpm build && cd target && zip -qr discount-expiry-notifier.zip ./*.js", + "type-check": "tsc --noEmit", + "check-formatting": "prettier --check **.ts", + "fix-formatting": "prettier --write **.ts" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.147" + }, + "dependencies": { + } +} \ No newline at end of file diff --git a/handlers/discount-expiry-notifier/riff-raff.yaml b/handlers/discount-expiry-notifier/riff-raff.yaml new file mode 100644 index 0000000000..d6ff3756e0 --- /dev/null +++ b/handlers/discount-expiry-notifier/riff-raff.yaml @@ -0,0 +1,24 @@ +stacks: + - support +regions: + - eu-west-1 +allowedStages: + - CODE + - PROD +deployments: + discount-expiry-notifier-cloudformation: + type: cloud-formation + app: discount-expiry-notifier + parameters: + templateStagePaths: + CODE: discount-expiry-notifier-CODE.template.json + PROD: discount-expiry-notifier-PROD.template.json + discount-expiry-notifier: + type: aws-lambda + parameters: + fileName: discount-expiry-notifier.zip + bucketSsmLookup: true + prefixStack: false + functionNames: + - discount-expiry-notifier-get-subs-with-expiring-discounts- + dependencies: [discount-expiry-notifier-cloudformation] diff --git a/handlers/discount-expiry-notifier/src/handlers/getSubsWithExpiringDiscounts.ts b/handlers/discount-expiry-notifier/src/handlers/getSubsWithExpiringDiscounts.ts new file mode 100644 index 0000000000..ad5a6a9ead --- /dev/null +++ b/handlers/discount-expiry-notifier/src/handlers/getSubsWithExpiringDiscounts.ts @@ -0,0 +1 @@ +export const handler = () => {}; diff --git a/handlers/discount-expiry-notifier/test/handlers/getSubsWithExpiringDiscounts.test.ts b/handlers/discount-expiry-notifier/test/handlers/getSubsWithExpiringDiscounts.test.ts new file mode 100644 index 0000000000..7d9b90ddcc --- /dev/null +++ b/handlers/discount-expiry-notifier/test/handlers/getSubsWithExpiringDiscounts.test.ts @@ -0,0 +1,5 @@ +describe('Handler', () => { + it('should handle successfully', () => { + expect(1).toEqual(1); + }); +}); diff --git a/handlers/discount-expiry-notifier/tsconfig.json b/handlers/discount-expiry-notifier/tsconfig.json new file mode 100644 index 0000000000..1864d58c15 --- /dev/null +++ b/handlers/discount-expiry-notifier/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 886b754ab1..cb669864b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,12 @@ importers: specifier: ^8.10.147 version: 8.10.147 + handlers/discount-expiry-notifier: + devDependencies: + '@types/aws-lambda': + specifier: ^8.10.147 + version: 8.10.147 + handlers/generate-product-catalog: devDependencies: '@aws-sdk/client-s3': @@ -5522,7 +5528,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/client-sts': 3.699.0 '@aws-sdk/core': 3.696.0 - '@aws-sdk/credential-provider-node': 3.699.0(@aws-sdk/client-sso-oidc@3.699.0(@aws-sdk/client-sts@3.699.0))(@aws-sdk/client-sts@3.699.0) + '@aws-sdk/credential-provider-node': 3.699.0(@aws-sdk/client-sso-oidc@3.699.0(@aws-sdk/client-sts@3.699.0))(@aws-sdk/client-sts@3.665.0) '@aws-sdk/middleware-host-header': 3.696.0 '@aws-sdk/middleware-logger': 3.696.0 '@aws-sdk/middleware-recursion-detection': 3.696.0