constructor()

in cdk/lib/stripe-webhook-endpoints.ts [25:244]


	constructor(scope: App, id: string, props: StripeWebhookEndpointsProps) {
		super(scope, id, props);

		const app = 'stripe-webhook-endpoints';

		// ---- Existing CFN template ---- //
		const yamlTemplateFilePath = path.join(
			__dirname,
			'../..',
			'handlers/stripe-webhook-endpoints/cfn.yaml',
		);
		new CfnInclude(this, 'YamlTemplate', {
			templateFile: yamlTemplateFilePath,
		});

		// ---- API-triggered lambda functions ---- //

		const paymentIntentIssuesLambda = new GuLambdaFunction(
			this,
			'payment-intent-issues-cdk-lambda',
			{
				app: app,
				description:
					'A lambda for handling payment intent issues (cancellation, failure, action required)',
				functionName: `stripe-payment-intent-issues-cdk-${this.stage}`,
				fileName: `${app}.jar`,
				handler: 'com.gu.paymentIntentIssues.Lambda::handler',
				runtime: Runtime.JAVA_21,
				memorySize: 1536,
				timeout: Duration.seconds(300),
				environment: {
					App: app,
					Stack: this.stack,
					Stage: this.stage,
				},
			},
		);

		const customerUpdatedLambda = new GuLambdaFunction(
			this,
			'customer-updated-cdk-lambda',
			{
				app: app,
				description: 'A lambda for handling customer updates',
				functionName: `stripe-customer-updated-cdk-${this.stage}`,
				fileName: `${app}.jar`,
				handler: 'com.gu.stripeCardUpdated.Lambda::apply',
				runtime: Runtime.JAVA_21,
				memorySize: 1536,
				timeout: Duration.seconds(900),
				environment: {
					App: app,
					Stack: this.stack,
					Stage: this.stage,
				},
			},
		);

		// Wire up the API
		// ---- API gateway ---- //
		const stripeWebhookEndpointsApi = new GuApiGatewayWithLambdaByPath(this, {
			app: 'stripe-webhook-endpoints',
			targets: [
				{
					path: '/payment-intent-issue',
					httpMethod: 'POST',
					lambda: paymentIntentIssuesLambda,
				},
				{
					path: '/customer-updated',
					httpMethod: 'POST',
					lambda: customerUpdatedLambda,
				},
			],
			// Create an alarm
			monitoringConfiguration: {
				snsTopicName: 'conversion-dev',
				http5xxAlarm: {
					tolerated5xxPercentage: 1,
				},
			},
		});

		// ---- Alarms ---- //
		const alarmName = (shortDescription: string) =>
			`Stripe Webhook Endpoints CDK ${this.stage} ${shortDescription}`;

		const alarmDescription = (description: string) =>
			`Impact - ${description}. Follow the process in https://docs.google.com/document/d/1_3El3cly9d7u_jPgTcRjLxmdG2e919zCLvmcFCLOYAk/edit`;

		new GuAlarm(this, 'ApiGateway4XXAlarmCDK', {
			app,
			alarmName: alarmName('API gateway 4XX response'),
			alarmDescription: alarmDescription(
				'Stripe Webhook Endpoints received an invalid request',
			),
			evaluationPeriods: 1,
			threshold: 1,
			snsTopicName: 'conversion-dev',
			actionsEnabled: this.stage === 'PROD',
			comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
			metric: new Metric({
				metricName: '4XXError',
				namespace: 'AWS/ApiGateway',
				statistic: 'Sum',
				period: Duration.seconds(300),
				dimensionsMap: {
					ApiName: `${app}-${this.stage}`,
				},
			}),
		});

		new GuAlarm(this, 'ApiGateway5XXAlarmCDK', {
			app,
			alarmName: alarmName('API gateway 5XX error'),
			alarmDescription: `stripe-webhook-endpoints-${this.stage} exceeded 1% 5XX error rate`,
			evaluationPeriods: 1,
			threshold: 1,
			actionsEnabled: this.stage === 'PROD',
			snsTopicName: 'conversion-dev',
			comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
			metric: new Metric({
				metricName: '5XXError',
				namespace: 'AWS/ApiGateway',
				statistic: 'Sum',
				period: Duration.seconds(60),
				dimensionsMap: {
					ApiName: `${app}-${this.stage}`,
				},
			}),
		});

		// ---- DNS ---- //
		const certificateArn = `arn:aws:acm:${this.region}:${this.account}:certificate/${props.certificateId}`;

		const cfnDomainName = new CfnDomainName(
			this,
			'StripeWebhookEndpointsDomainName',
			{
				domainName: props.domainName,
				regionalCertificateArn: certificateArn,
				endpointConfiguration: {
					types: ['REGIONAL'],
				},
			},
		);

		new CfnBasePathMapping(this, 'BasePathMapping', {
			domainName: cfnDomainName.ref,
			// Uncomment the lines below to reroute traffic to the new API Gateway instance
			restApiId: stripeWebhookEndpointsApi.api.restApiId,
			stage: stripeWebhookEndpointsApi.api.deploymentStage.stageName,
			// Uncomment the lines below to reroute traffic to the old (existing) API Gateway instance
			// restApiId: yamlDefinedResources.getResource("ServerlessRestApi").ref,
			// stage: props.stage,
		});

		new CfnRecordSet(this, 'DNSRecord', {
			name: props.domainName,
			type: 'CNAME',
			hostedZoneId: props.hostedZoneId,
			ttl: '120',
			resourceRecords: [cfnDomainName.attrRegionalDomainName],
		});

		// ---- Apply policies ---- //
		const ssmInlinePolicy: Policy = new Policy(this, 'SSM inline policy', {
			statements: [
				new PolicyStatement({
					effect: Effect.ALLOW,
					actions: ['ssm:GetParametersByPath'],
					resources: [
						`arn:aws:ssm:${this.region}:${this.account}:parameter/${props.stage}/membership/payment-intent-issues/*`,
					],
				}),
			],
		});

		const s3InlinePolicyForPaymentIntentIssues: Policy = new Policy(
			this,
			'S3 inline policy',
			{
				statements: [
					new PolicyStatement({
						effect: Effect.ALLOW,
						actions: ['s3:GetObject'],
						resources: ['arn:aws:s3::*:membership-dist/*'],
					}),
				],
			},
		);

		const s3InlinePolicyForCustomerUpdated: Policy = new Policy(
			this,
			'S3 inline policy For Customer Updated lambda',
			{
				statements: [
					new PolicyStatement({
						effect: Effect.ALLOW,
						actions: ['s3:GetObject'],
						resources: [
							`arn:aws:s3::*:membership-dist/*`,
							`arn:aws:s3:::gu-reader-revenue-private/membership/support-service-lambdas/${props.stage}/zuoraRest-${props.stage}.*.json`,
							`arn:aws:s3:::gu-reader-revenue-private/membership/support-service-lambdas/${props.stage}/trustedApi-${props.stage}.*.json`,
							`arn:aws:s3:::gu-reader-revenue-private/membership/support-service-lambdas/${props.stage}/stripe-${props.stage}.*.json`,
						],
					}),
				],
			},
		);

		paymentIntentIssuesLambda.role?.attachInlinePolicy(ssmInlinePolicy);
		paymentIntentIssuesLambda.role?.attachInlinePolicy(
			s3InlinePolicyForPaymentIntentIssues,
		);
		customerUpdatedLambda.role?.attachInlinePolicy(ssmInlinePolicy);
		customerUpdatedLambda.role?.attachInlinePolicy(
			s3InlinePolicyForCustomerUpdated,
		);
	}