From 19a5ff5dc61a4fd031c53e113c64a085ac06f544 Mon Sep 17 00:00:00 2001 From: Rajil Paloth Date: Tue, 17 Mar 2026 13:04:21 +0530 Subject: [PATCH 1/3] New serverless pattern - eventbridge-scheduler-ses-abandoned-cart-notification --- .../Architecture.png | Bin 0 -> 40840 bytes .../README.md | 304 +++++++++++++ .../example-pattern.json | 59 +++ .../main.tf | 409 ++++++++++++++++++ .../notification_processor.py | 260 +++++++++++ 5 files changed, 1032 insertions(+) create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/Architecture.png create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/README.md create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/main.tf create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.py diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/Architecture.png b/eventbridge-scheduler-ses-abandoned-cart-notification/Architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b86b5b771ac8229a25ae86b584f8fa42a66dc220 GIT binary patch literal 40840 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 1.0) installed + +### Prerequisites — Amazon SES Identity + +This pattern sends emails through Amazon SES. You **must** have a verified SES identity (email address or domain) before deploying. + +1. **Verify a sender email address** (or domain) in the [SES console](https://console.aws.amazon.com/ses/home#/verified-identities) or via the CLI: + + ```bash + aws ses verify-email-identity --email-address noreply@yourdomain.com + ``` + +2. **Check your inbox** and click the verification link sent by AWS. + +3. **If your SES account is in sandbox mode** (the default for new accounts), you must also verify every **recipient** email address and ensure `ses:SendEmail` permissions include the recipient identity. For the seed test data included in this pattern: + + ```bash + aws ses verify-email-identity --email-address rajilpaloth@gmail.com + ``` + + > **Note:** In sandbox mode, SES requires `ses:SendEmail` permission for both the sender and recipient identities. If your SES account has **production access** enabled, recipient verification and permissions are not required. + +4. **Note your SES identity ARN** — you will need it during deployment: + + ``` + arn:aws:ses:{region}:{account-id}:identity/noreply@yourdomain.com + ``` + + You can retrieve it with: + + ```bash + echo "arn:aws:ses:$(aws configure get region):$(aws sts get-caller-identity --query Account --output text):identity/noreply@yourdomain.com" + ``` + +5. Confirm both identities show `"VerificationStatus": "Success"`: + + ```bash + aws ses get-identity-verification-attributes \ + --identities noreply@yourdomain.com rajilpaloth@gmail.com + ``` + +### Prerequisites — DynamoDB Table Population + +This pattern deploys the DynamoDB table and seeds it with three test records for demonstration purposes. In a production environment, you will need a **separate system or mechanism** to populate the DynamoDB table with real customer data whenever a cart is abandoned. Common approaches include: + +- An **API Gateway + Lambda** endpoint called by your e-commerce application when a cart is abandoned +- A **DynamoDB Streams** consumer that reacts to cart updates and sets the `CartAbandoned` flag +- A **Step Functions** workflow that monitors cart activity and marks carts as abandoned after a timeout period +- A direct **SDK write** from your application backend + +The notification processor Lambda in this pattern only **reads** the table and **updates** the `NotificationSent` flag — it does not create or manage cart records. + +**DynamoDB record schema expected by the Lambda function:** + +| Attribute | Type | Description | +|---|---|---| +| `CustomerId` | String (Hash Key) | Unique customer identifier | +| `Email` | String | Customer email address | +| `CustomerName` | String | Customer display name | +| `CartAbandoned` | String (`"true"` / `"false"`) | Whether the cart is abandoned (GSI hash key) | +| `NotificationSent` | String (`"true"` / `"false"`) | Whether the notification email has been sent | +| `CartItems` | List of Maps | Items in the cart (`ItemName`, `Price`) | +| `CartTotal` | Number | Total cart value | +| `CartAbandonedAt` | String (ISO 8601) | Timestamp when the cart was abandoned | + +## Architecture + +![Architecture diagram](Architecture.png) + +EventBridge Scheduler (rate 1 hour) +│ +├── on failure ──▶ SQS DLQ +│ +▼ +Notification Processor Lambda +│ +├── READ ──▶ DynamoDB (abandoned-carts) +│ query CartAbandoned = "true" +│ filter NotificationSent = "false" +│ +└── SEND ──▶ Amazon SES +per-customer abandoned cart email +then mark NotificationSent = "true" + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` + +1. Change directory to the pattern directory: + + ```bash + cd serverless-patterns/eventbridge-scheduler-ses-abandoned-cart-notification + ``` + +1. Initialize Terraform: + + ```bash + terraform init + ``` + +1. Review the execution plan: + + ```bash + terraform plan \ + -var="aws_region=us-east-1" \ + -var="prefix=cartnotify" \ + -var="ses_identity_arn=arn:aws:ses:us-east-1:123456789012:identity/noreply@yourdomain.com" \ + -var="sender_email=noreply@yourdomain.com" + ``` + + Replace the SES identity ARN, sender email, and region with your actual values. + +1. Deploy the resources: + + ```bash + terraform apply \ + -var="aws_region=us-east-1" \ + -var="prefix=cartnotify" \ + -var="ses_identity_arn=arn:aws:ses:us-east-1:123456789012:identity/noreply@yourdomain.com" \ + -var="sender_email=noreply@yourdomain.com" + ``` + + Type `yes` when prompted. Deployment takes approximately 1–2 minutes. + +1. Note the outputs from the Terraform deployment process. These contain the resource names and ARNs used for testing: + + ```bash + terraform output + ``` + +## How it works + +1. **EventBridge Scheduler** fires on the configured schedule (default: every hour) and invokes the **Notification Processor Lambda** function. + +2. The Lambda function queries the **DynamoDB Global Secondary Index** (`CartAbandonedIndex`) to retrieve all records where `CartAbandoned = "true"`. + +3. For each abandoned cart record, the function checks the `NotificationSent` attribute: + - If `"true"` → the customer has already been emailed, so the record is **skipped**. + - If `"false"` → the function proceeds to send an email. + +4. The function builds a **personalised HTML email** containing the customer's name, cart items, cart total, and a call-to-action button, then sends it via **Amazon SES**. + +5. After a successful send, the function **updates the DynamoDB record**, setting `NotificationSent = "true"` and recording a `NotifiedAt` timestamp. This ensures the customer is **never emailed twice** for the same abandoned cart, even if the scheduler fires again. + +6. If the scheduler fails to invoke the Lambda after 3 retries, the event is sent to the **SQS Dead-Letter Queue** for investigation. + +**Seed test data included:** + +| CustomerId | Email | CartAbandoned | NotificationSent | Expected Behaviour | +|---|---|---|---|---| +| `cust-001` | `rajilpaloth@gmail.com` | `true` | `false` | ✅ Will receive email | +| `cust-002` | `activecustomer@example.com` | `false` | `false` | ⏭️ Not abandoned — not queried | +| `cust-003` | `alreadynotified@example.com` | `true` | `true` | ⏭️ Already notified — skipped | + +## Testing + +1. **Invoke the Lambda function manually** (without waiting for the schedule): + + ```bash + aws lambda invoke \ + --function-name cartnotify-notification-processor \ + --cli-binary-format raw-in-base64-out \ + --payload '{ + "source": "manual-test", + "taskType": "abandoned-cart-notification" + }' \ + /dev/stdout 2>/dev/null | jq . + ``` + + Expected response: + + ```json + { + "statusCode": 200, + "body": "{\"invokedAt\": \"2025-01-15T12:00:05Z\", \"notificationsSent\": 1, \"skipped\": 1, \"errors\": 0}" + } + ``` + +2. **Check the recipient inbox** (`rajilpaloth@gmail.com`) for the abandoned cart email. Check the spam/junk folder if it does not appear in the inbox. + +3. **Verify the DynamoDB record was updated:** + + ```bash + aws dynamodb get-item \ + --table-name cartnotify-abandoned-carts \ + --key '{"CustomerId": {"S": "cust-001"}}' \ + --query "Item.{NotificationSent:NotificationSent.S,NotifiedAt:NotifiedAt.S}" \ + --output table + ``` + + Expected: + + ``` + ────────────────────────────────────────── + | GetItem | + +------------------+---------------------+ + | NotificationSent | NotifiedAt | + +------------------+---------------------+ + | true | 2025-01-15T12:00:05Z| + +------------------+---------------------+ + ``` + +4. **Verify idempotency** by invoking the Lambda again: + + ```bash + aws lambda invoke \ + --function-name cartnotify-notification-processor \ + --cli-binary-format raw-in-base64-out \ + --payload '{"source": "idempotency-test"}' \ + /dev/stdout 2>/dev/null | jq . + ``` + + Expected — no duplicate emails sent: + + ```json + { + "statusCode": 200, + "body": "{\"invokedAt\": \"...\", \"notificationsSent\": 0, \"skipped\": 2, \"errors\": 0}" + } + ``` + +5. **Reset the test record** to re-test: + + ```bash + aws dynamodb update-item \ + --table-name cartnotify-abandoned-carts \ + --key '{"CustomerId": {"S": "cust-001"}}' \ + --update-expression "SET NotificationSent = :f REMOVE NotifiedAt" \ + --expression-attribute-values '{":f": {"S": "false"}}' + ``` + +6. **Check Lambda logs** for detailed execution information: + + ```bash + aws logs tail /aws/lambda/cartnotify-notification-processor --follow + ``` + +7. **Check the DLQ** for any failed scheduler invocations: + + ```bash + aws sqs get-queue-attributes \ + --queue-url $(terraform output -raw dlq_queue_url) \ + --attribute-names ApproximateNumberOfMessages + ``` + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| `MessageRejected: Email address is not verified` | SES sandbox — recipient not verified | Run `aws ses verify-email-identity --email-address rajilpaloth@gmail.com` and click the verification link | +| `AccessDenied` on `ses:SendEmail` | SES identity ARN mismatch | Verify the `ses_identity_arn` variable matches your sender email exactly | +| Lambda returns `notificationsSent: 0` | All abandoned carts already notified | Reset with the `update-item` command in the Testing section | +| No items returned from GSI query | GSI not yet backfilled | Wait 30 seconds after deploy for the GSI to populate | +| Schedule never fires | Schedule expression typo | Run `aws scheduler get-schedule --name cartnotify-abandoned-cart-notify` | +| DLQ filling up | Lambda timeout or SES errors | Check CloudWatch logs and increase the Lambda timeout if needed | + +## Cleanup + +1. Delete the stack: + + ```bash + terraform destroy \ + -var="aws_region=us-east-1" \ + -var="prefix=cartnotify" \ + -var="ses_identity_arn=arn:aws:ses:us-east-1:123456789012:identity/noreply@yourdomain.com" \ + -var="sender_email=noreply@yourdomain.com" \ + -auto-approve + ``` + +2. Confirm all resources have been removed: + + ```bash + aws dynamodb describe-table --table-name cartnotify-abandoned-carts 2>&1 | grep -q "ResourceNotFoundException" && echo "Table deleted" || echo "Table still exists" + aws lambda get-function --function-name cartnotify-notification-processor 2>&1 | grep -q "ResourceNotFoundException" && echo "Lambda deleted" || echo "Lambda still exists" + ``` + +3. (Optional) Remove the SES verified identities if no longer needed: + + ```bash + aws ses delete-verified-email-identity --email-address noreply@yourdomain.com + aws ses delete-verified-email-identity --email-address rajilpaloth@gmail.com + ``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json new file mode 100644 index 0000000000..8616bc19b6 --- /dev/null +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json @@ -0,0 +1,59 @@ +{ + "title": "Step Functions to Athena", + "description": "Create a Step Functions workflow to query Amazon Athena.", + "language": "Python", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to use an AWS Step Functions state machine to query Athena and get the results. This pattern is leveraging the native integration between these 2 services which means only JSON-based, structured language is used to define the implementation.", + "With Amazon Athena you can get up to 1000 results per invocation of the GetQueryResults method and this is the reason why the Step Function has a loop to get more results. The results are sent to a Map which can be configured to handle (the DoSomething state) the items in parallel or one by one by modifying the max_concurrency parameter.", + "This pattern deploys one Step Functions, two S3 Buckets, one Glue table and one Glue database." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/sfn-athena-cdk-python", + "templateURL": "serverless-patterns/sfn-athena-cdk-python", + "projectFolder": "sfn-athena-cdk-python", + "templateFile": "sfn_athena_cdk_python_stack.py" + } + }, + "resources": { + "bullets": [ + { + "text": "Call Athena with Step Functions", + "link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html" + }, + { + "text": "Amazon Athena - Serverless Interactive Query Service", + "link": "https://aws.amazon.com/athena/" + } + ] + }, + "deploy": { + "text": [ + "sam deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk delete." + ] + }, + "authors": [ + { + "name": "Your name", + "image": "link-to-your-photo.jpg", + "bio": "Your bio.", + "linkedin": "linked-in-ID", + "twitter": "twitter-handle" + } + ] +} diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf b/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf new file mode 100644 index 0000000000..d26462f2c5 --- /dev/null +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf @@ -0,0 +1,409 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +############################################################ +# Variables +############################################################ + +variable "aws_region" { + description = "AWS region for resources (e.g. us-east-1, us-west-2)" + type = string + + validation { + condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]+$", var.aws_region)) + error_message = "Must be a valid AWS region (e.g. us-east-1, eu-west-2)." + } +} + +variable "prefix" { + description = "Unique prefix for all resource names" + type = string + + validation { + condition = can(regex("^[a-z0-9][a-z0-9\\-]{1,20}$", var.prefix)) + error_message = "Prefix must be 2-21 lowercase alphanumeric characters or hyphens." + } +} + +variable "ses_identity_arn" { + description = "ARN of the verified SES identity (email or domain) used as the sender (e.g. arn:aws:ses:us-east-1:123456789012:identity/noreply@example.com)" + type = string + + validation { + condition = can(regex("^arn:aws:ses:[a-z0-9-]+:[0-9]{12}:identity/.+$", var.ses_identity_arn)) + error_message = "Must be a valid SES identity ARN (e.g. arn:aws:ses:us-east-1:123456789012:identity/noreply@example.com)." + } +} + +variable "sender_email" { + description = "Verified SES sender email address (must match the SES identity)" + type = string + + validation { + condition = can(regex("^[^@]+@[^@]+\\.[^@]+$", var.sender_email)) + error_message = "Must be a valid email address." + } +} + +variable "schedule_expression" { + description = "EventBridge Scheduler expression (e.g. rate(1 hour), rate(5 minutes) for testing)" + type = string + default = "rate(1 hour)" +} + +variable "log_retention_days" { + description = "CloudWatch log retention in days (0 = never expire)" + type = number + default = 14 +} + +############################################################ +# Data Sources +############################################################ + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +############################################################ +# 1. DYNAMODB TABLE (abandoned cart records) +# +# NOTE: hash_key is deprecated in the AWS provider and will +# be replaced by a key_schema block in a future major +# version. The replacement syntax is not yet available in +# the 5.x provider, so hash_key is used here. The +# deprecation warning can be safely ignored. +############################################################ + +resource "aws_dynamodb_table" "abandoned_carts" { + name = "${var.prefix}-abandoned-carts" + billing_mode = "PAY_PER_REQUEST" + hash_key = "CustomerId" + + attribute { + name = "CustomerId" + type = "S" + } + + attribute { + name = "CartAbandoned" + type = "S" + } + + # GSI to efficiently query only abandoned carts + global_secondary_index { + name = "CartAbandonedIndex" + hash_key = "CartAbandoned" + projection_type = "ALL" + } + + tags = { + Project = "${var.prefix}-abandoned-cart-notifications" + } +} + +# ── Seed test data ── + +resource "aws_dynamodb_table_item" "test_user_abandoned" { + table_name = aws_dynamodb_table.abandoned_carts.name + hash_key = aws_dynamodb_table.abandoned_carts.hash_key + + item = jsonencode({ + CustomerId = { S = "cust-001" } + Email = { S = "rajilpaloth@gmail.com" } + CustomerName = { S = "Rajil Paloth" } + CartAbandoned = { S = "true" } + NotificationSent = { S = "false" } + CartItems = { L = [ + { M = { ItemName = { S = "Wireless Headphones" }, Price = { N = "79.99" } } }, + { M = { ItemName = { S = "Phone Case" }, Price = { N = "19.99" } } } + ] } + CartTotal = { N = "99.98" } + CartAbandonedAt = { S = "2025-01-15T08:30:00Z" } + }) +} + +resource "aws_dynamodb_table_item" "test_user_active" { + table_name = aws_dynamodb_table.abandoned_carts.name + hash_key = aws_dynamodb_table.abandoned_carts.hash_key + + item = jsonencode({ + CustomerId = { S = "cust-002" } + Email = { S = "activecustomer@example.com" } + CustomerName = { S = "Active Customer" } + CartAbandoned = { S = "false" } + NotificationSent = { S = "false" } + CartItems = { L = [ + { M = { ItemName = { S = "Laptop Stand" }, Price = { N = "49.99" } } } + ] } + CartTotal = { N = "49.99" } + CartAbandonedAt = { S = "N/A" } + }) +} + +resource "aws_dynamodb_table_item" "test_user_already_notified" { + table_name = aws_dynamodb_table.abandoned_carts.name + hash_key = aws_dynamodb_table.abandoned_carts.hash_key + + item = jsonencode({ + CustomerId = { S = "cust-003" } + Email = { S = "alreadynotified@example.com" } + CustomerName = { S = "Already Notified" } + CartAbandoned = { S = "true" } + NotificationSent = { S = "true" } + CartItems = { L = [ + { M = { ItemName = { S = "USB Cable" }, Price = { N = "12.99" } } } + ] } + CartTotal = { N = "12.99" } + CartAbandonedAt = { S = "2025-01-14T10:00:00Z" } + }) +} + +############################################################ +# 2. NOTIFICATION PROCESSOR LAMBDA +############################################################ + +resource "aws_iam_role" "processor_role" { + name = "${var.prefix}-notification-processor-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { Service = "lambda.amazonaws.com" } + }] + }) +} + +resource "aws_iam_role_policy_attachment" "processor_basic" { + role = aws_iam_role.processor_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy" "processor_dynamodb" { + name = "${var.prefix}-processor-dynamodb" + role = aws_iam_role.processor_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "DynamoDBReadWrite" + Effect = "Allow" + Action = [ + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:UpdateItem" + ] + Resource = [ + aws_dynamodb_table.abandoned_carts.arn, + "${aws_dynamodb_table.abandoned_carts.arn}/index/*" + ] + }] + }) +} + +resource "aws_iam_role_policy" "processor_ses" { + name = "${var.prefix}-processor-ses" + role = aws_iam_role.processor_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "SendEmail" + Effect = "Allow" + Action = "ses:SendEmail" + Resource = var.ses_identity_arn + }] + }) +} + +resource "aws_cloudwatch_log_group" "processor_logs" { + name = "/aws/lambda/${var.prefix}-notification-processor" + retention_in_days = var.log_retention_days +} + +data "archive_file" "processor_zip" { + type = "zip" + source_file = "${path.module}/notification_processor.py" + output_path = "${path.module}/notification_processor.zip" +} + +resource "aws_lambda_function" "processor" { + function_name = "${var.prefix}-notification-processor" + role = aws_iam_role.processor_role.arn + handler = "notification_processor.lambda_handler" + runtime = "python3.14" + timeout = 60 + memory_size = 256 + filename = data.archive_file.processor_zip.output_path + source_code_hash = data.archive_file.processor_zip.output_base64sha256 + + environment { + variables = { + DYNAMODB_TABLE = aws_dynamodb_table.abandoned_carts.name + SES_IDENTITY_ARN = var.ses_identity_arn + SENDER_EMAIL = var.sender_email + PREFIX = var.prefix + } + } + + depends_on = [ + aws_cloudwatch_log_group.processor_logs, + aws_iam_role_policy_attachment.processor_basic, + aws_iam_role_policy.processor_dynamodb, + aws_iam_role_policy.processor_ses, + ] +} + +############################################################ +# 3. SQS DEAD-LETTER QUEUE +############################################################ + +resource "aws_sqs_queue" "scheduler_dlq" { + name = "${var.prefix}-cart-notify-dlq" + message_retention_seconds = 1209600 # 14 days +} + +############################################################ +# 4. EVENTBRIDGE SCHEDULER +############################################################ + +resource "aws_iam_role" "scheduler_role" { + name = "${var.prefix}-cart-notify-scheduler-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { Service = "scheduler.amazonaws.com" } + }] + }) +} + +resource "aws_iam_role_policy" "scheduler_permissions" { + name = "${var.prefix}-scheduler-permissions" + role = aws_iam_role.scheduler_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "InvokeLambda" + Effect = "Allow" + Action = "lambda:InvokeFunction" + Resource = aws_lambda_function.processor.arn + }, + { + Sid = "SendToDLQ" + Effect = "Allow" + Action = "sqs:SendMessage" + Resource = aws_sqs_queue.scheduler_dlq.arn + } + ] + }) +} + +resource "aws_sqs_queue_policy" "allow_scheduler" { + queue_url = aws_sqs_queue.scheduler_dlq.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "AllowSchedulerSendMessage" + Effect = "Allow" + Principal = { Service = "scheduler.amazonaws.com" } + Action = "sqs:SendMessage" + Resource = aws_sqs_queue.scheduler_dlq.arn + Condition = { + ArnEquals = { + "aws:SourceArn" = "arn:aws:scheduler:${var.aws_region}:${data.aws_caller_identity.current.account_id}:schedule/default/${var.prefix}-abandoned-cart-notify" + } + } + }] + }) +} + +resource "aws_cloudwatch_log_group" "scheduler_logs" { + name = "/aws/scheduler/${var.prefix}-abandoned-cart-notify" + retention_in_days = var.log_retention_days +} + +resource "aws_scheduler_schedule" "abandoned_cart_notify" { + name = "${var.prefix}-abandoned-cart-notify" + schedule_expression = var.schedule_expression + + flexible_time_window { + mode = "OFF" + } + + target { + arn = aws_lambda_function.processor.arn + role_arn = aws_iam_role.scheduler_role.arn + + input = jsonencode({ + source = "eventbridge-scheduler" + taskType = "abandoned-cart-notification" + invokedAt = "REPLACED_AT_RUNTIME" + }) + + retry_policy { + maximum_retry_attempts = 3 + maximum_event_age_in_seconds = 3600 + } + + dead_letter_config { + arn = aws_sqs_queue.scheduler_dlq.arn + } + } + + depends_on = [aws_cloudwatch_log_group.scheduler_logs] +} + +############################################################ +# 5. OUTPUTS +############################################################ + +output "prefix" { + value = var.prefix +} + +output "schedule_name" { + value = aws_scheduler_schedule.abandoned_cart_notify.name +} + +output "schedule_arn" { + value = aws_scheduler_schedule.abandoned_cart_notify.arn +} + +output "lambda_function_name" { + value = aws_lambda_function.processor.function_name +} + +output "dynamodb_table_name" { + value = aws_dynamodb_table.abandoned_carts.name +} + +output "dlq_queue_url" { + value = aws_sqs_queue.scheduler_dlq.url +} + +output "ses_identity_arn" { + value = var.ses_identity_arn +} + +output "sender_email" { + value = var.sender_email +} \ No newline at end of file diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.py b/eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.py new file mode 100644 index 0000000000..1ab1fa3481 --- /dev/null +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.py @@ -0,0 +1,260 @@ +import boto3 +import json +import os +import logging +from datetime import datetime, timezone + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +dynamodb = boto3.resource("dynamodb") +ses = boto3.client("ses") + +TABLE_NAME = os.environ["DYNAMODB_TABLE"] +SENDER_EMAIL = os.environ["SENDER_EMAIL"] + +table = dynamodb.Table(TABLE_NAME) + + +def lambda_handler(event, context): + """ + Notification Processor — invoked hourly by EventBridge Scheduler. + + 1. Queries the DynamoDB GSI for records where CartAbandoned = "true" + 2. Filters for NotificationSent = "false" + 3. Sends a personalised abandoned-cart email via SES + 4. Marks NotificationSent = "true" so the customer is not emailed again + """ + logger.info("Received event: %s", json.dumps(event)) + invoked_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # ── 1. Query the GSI for all abandoned carts ── + abandoned = _get_abandoned_carts() + logger.info("Found %d abandoned cart(s) needing notification", len(abandoned)) + + if not abandoned: + return _response(invoked_at, sent=0, skipped=0, errors=0) + + sent = 0 + skipped = 0 + errors = 0 + + for record in abandoned: + customer_id = record.get("CustomerId", "unknown") + email = record.get("Email", "") + name = record.get("CustomerName", "Customer") + notification_sent = record.get("NotificationSent", "false") + + # ── 2. Skip if already notified ── + if notification_sent == "true": + logger.info("Skipping %s — already notified", customer_id) + skipped += 1 + continue + + # ── 3. Skip if no email address ── + if not email: + logger.warning("Skipping %s — no email address", customer_id) + skipped += 1 + continue + + # ── 4. Build and send the email ── + try: + cart_items = _format_cart_items(record.get("CartItems", [])) + cart_total = record.get("CartTotal", "0.00") + abandoned_at = record.get("CartAbandonedAt", "recently") + + _send_email(email, name, cart_items, cart_total, abandoned_at) + logger.info("Sent notification to %s (%s)", customer_id, email) + + # ── 5. Mark as notified ── + _mark_notified(customer_id, invoked_at) + sent += 1 + + except Exception as e: + logger.error( + "Failed to notify %s (%s): %s", customer_id, email, str(e) + ) + errors += 1 + + return _response(invoked_at, sent=sent, skipped=skipped, errors=errors) + + +# ────────────────────────────────────────── +# DynamoDB helpers +# ────────────────────────────────────────── + + +def _get_abandoned_carts() -> list: + """ + Query the GSI for all records with CartAbandoned = 'true'. + The GSI returns all abandoned carts; we filter NotificationSent + in application code for flexibility. + """ + items = [] + last_key = None + + while True: + query_params = { + "IndexName": "CartAbandonedIndex", + "KeyConditionExpression": "CartAbandoned = :abandoned", + "ExpressionAttributeValues": {":abandoned": "true"}, + } + + if last_key: + query_params["ExclusiveStartKey"] = last_key + + response = table.query(**query_params) + items.extend(response.get("Items", [])) + + last_key = response.get("LastEvaluatedKey") + if not last_key: + break + + return items + + +def _mark_notified(customer_id: str, notified_at: str) -> None: + """Set NotificationSent = 'true' and record the timestamp.""" + table.update_item( + Key={"CustomerId": customer_id}, + UpdateExpression=( + "SET NotificationSent = :sent, NotifiedAt = :ts" + ), + ExpressionAttributeValues={ + ":sent": "true", + ":ts": notified_at, + }, + ) + + +# ────────────────────────────────────────── +# SES helpers +# ────────────────────────────────────────── + + +def _send_email( + to_email: str, + customer_name: str, + cart_items_html: str, + cart_total: str, + abandoned_at: str, +) -> None: + """Send a personalised abandoned-cart email via SES.""" + + subject = f"{customer_name}, you left something in your cart!" + + html_body = f""" + + + + + + + + + """ + + text_body = ( + f"Hi {customer_name},\n\n" + f"You left items in your cart on {abandoned_at}.\n" + f"Cart Total: ${cart_total}\n\n" + f"Return to your cart: https://example.com/cart\n\n" + f"If you've already completed your purchase, please disregard." + ) + + ses.send_email( + Source=SENDER_EMAIL, + Destination={"ToAddresses": [to_email]}, + Message={ + "Subject": {"Data": subject, "Charset": "UTF-8"}, + "Body": { + "Html": {"Data": html_body, "Charset": "UTF-8"}, + "Text": {"Data": text_body, "Charset": "UTF-8"}, + }, + }, + ) + + +def _format_cart_items(cart_items: list) -> str: + """Convert DynamoDB cart items list into an HTML table.""" + if not cart_items: + return "

Your cart items are waiting for you.

" + + rows = "" + for item in cart_items: + name = item.get("ItemName", "Item") + price = item.get("Price", "0.00") + # Handle both Decimal (from DynamoDB resource) and string + rows += f"{name}${price}\n" + + return f""" + + + + + + {rows} + +
ItemPrice
+ """ + + +# ────────────────────────────────────────── +# Response helper +# ────────────────────────────────────────── + + +def _response(invoked_at: str, sent: int, skipped: int, errors: int) -> dict: + """Build a structured Lambda response.""" + summary = { + "invokedAt": invoked_at, + "notificationsSent": sent, + "skipped": skipped, + "errors": errors, + } + logger.info("Execution summary: %s", json.dumps(summary)) + return {"statusCode": 200, "body": json.dumps(summary)} \ No newline at end of file From ed8ef2c7dd964ad8b11615ff6e807a650fa020b8 Mon Sep 17 00:00:00 2001 From: Rajil Paloth Date: Tue, 17 Mar 2026 13:32:55 +0530 Subject: [PATCH 2/3] New serverless pattern - eventbridge-scheduler-ses-abandoned-cart-notification --- .../example-pattern.json | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json index 8616bc19b6..230b6bc6fb 100644 --- a/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json @@ -1,40 +1,39 @@ { - "title": "Step Functions to Athena", - "description": "Create a Step Functions workflow to query Amazon Athena.", + "title": "EventBridge Scheduler + SES Integration- Per-customer notification scheduling (abandoned cart, billing reminders)", + "description": "Create a EventBridge scheduler which sends per customer notifcations for abandoned carts and billing reminders using SES.", "language": "Python", - "level": "200", - "framework": "AWS CDK", + "level": "300", + "framework": "Terraform", "introBox": { "headline": "How it works", "text": [ - "This sample project demonstrates how to use an AWS Step Functions state machine to query Athena and get the results. This pattern is leveraging the native integration between these 2 services which means only JSON-based, structured language is used to define the implementation.", - "With Amazon Athena you can get up to 1000 results per invocation of the GetQueryResults method and this is the reason why the Step Function has a loop to get more results. The results are sent to a Map which can be configured to handle (the DoSomething state) the items in parallel or one by one by modifying the max_concurrency parameter.", - "This pattern deploys one Step Functions, two S3 Buckets, one Glue table and one Glue database." + "This pattern demonstrates how to use Amazon EventBridge Scheduler to drive per-customer abandoned cart email notifications on an hourly cadence. A Lambda function, invoked by the scheduler, queries a DynamoDB GSI for customers with abandoned carts that have not yet been notified, sends each a personalised HTML email via Amazon SES, and marks the record as notified to prevent duplicate emails. The pattern includes idempotent notification logic, seed test data, a dead-letter queue for failed scheduler invocations, and least-privilege IAM policies scoped to the specific SES identity and DynamoDB table." ] }, "gitHub": { "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/sfn-athena-cdk-python", - "templateURL": "serverless-patterns/sfn-athena-cdk-python", - "projectFolder": "sfn-athena-cdk-python", - "templateFile": "sfn_athena_cdk_python_stack.py" + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-scheduler-ses-abandoned-cart-notification", + "templateURL": "serverless-patterns/eventbridge-scheduler-ses-abandoned-cart-notification", + "projectFolder": "eventbridge-scheduler-ses-abandoned-cart-notification", + "templateFile": "main.tf" } }, "resources": { "bullets": [ { - "text": "Call Athena with Step Functions", - "link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html" + "text": "Verify Sender Email Address", + "link": "https://console.aws.amazon.com/ses/home#/verified-identities" }, { - "text": "Amazon Athena - Serverless Interactive Query Service", - "link": "https://aws.amazon.com/athena/" + "text": "Request production access (Moving out of the Amazon SES sandbox)", + "link": "https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html" } ] }, "deploy": { "text": [ - "sam deploy" + "terraform init", + "terraform apply" ] }, "testing": { @@ -44,16 +43,16 @@ }, "cleanup": { "text": [ - "Delete the stack: cdk delete." + "terraform destroy", + "terraform show" ] }, "authors": [ { - "name": "Your name", - "image": "link-to-your-photo.jpg", - "bio": "Your bio.", - "linkedin": "linked-in-ID", - "twitter": "twitter-handle" + "name": "Rajil Paloth", + "image": "https://i.ibb.co/r2TsqGf6/Passport-size.jpg", + "bio": "ProServe Delivery Consultant at AWS", + "linkedin": "paloth" } ] } From 691d54abcf54653d0a8139a0acde5a201ae6daea Mon Sep 17 00:00:00 2001 From: Rajil Paloth Date: Fri, 17 Apr 2026 20:19:47 +0530 Subject: [PATCH 3/3] Made requested changes --- .../Architecture.png | Bin 40840 -> 50819 bytes .../README.md | 58 ++++++++---------- .../example-pattern.json | 4 +- .../main.tf | 13 ++-- .../notification_processor.zip | Bin 0 -> 3047 bytes 5 files changed, 38 insertions(+), 37 deletions(-) create mode 100644 eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.zip diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/Architecture.png b/eventbridge-scheduler-ses-abandoned-cart-notification/Architecture.png index b86b5b771ac8229a25ae86b584f8fa42a66dc220..3db1999236324ee04388d5b08ba227824267eb0b 100644 GIT binary patch literal 50819 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 bA_@E(dUhBZd^7ly00000NkvXXu0mjfusV}f delta 31939 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 z z&y%qkAkwTdcmr7UO$<&|QU;v}+!m31zU{H>KikmHOyadDD?a{2_-`Cl)2F4n0>Oa%TXD3KVE zwi1zl36X?@;-*TUdVMRFu?(H3zwb3cL!XTX_TkW+x>q0+=>iBJx>T6TCjuz}FKFVm zrtC<~EPyyx&$6^PIg>nxX8ouHsW`+c&v}g0^Vs%bNWY%v(0(MLoDWy_Tw#v^p8$(^ z6$q7aF@q-p-#nLnB9K<7kgqTuEBc_r>B$0^5q;r-65E76>~qbA$-1Vf z{~2iHX#=HE8Z>-B1*PW-KkJ)RPk~%T<5L5O^R-7M(3;=y9a}p^;d9DsUDCQ#Q~@xd z==HBrzO~Z8kn?}OM)1Bu7nlOl4!m$fk?eqq;tM-E5IspNkz}B%MyaA3HnB=MP~6Ah zT8=^AspL}rYGKl@olk-8Vs7>&$P~Yd*-h~M20~UA7H4&;xKjIjrFjPMNDE$ck~^)- zs-F5(sw^}QQ?|DufTXMIqBo5ZR2ts~YfVJ?vk=bL2pz{kFqc2B9*gzQD*u6(> zq^+INIOz_=()2(^2moBbU^DVv6ME?L4V{7c_LuhOWdNG0*8H3o&_XK>+-b+tguwN5 zPqoEXG6Gn{)t6!QRizSNESzgo?pi3ekOhip+5Q(EZ@ z_lXoe${Ic-_$nLt6PFVAWbxvrh#elbq=e=}V9IoDhFNf_c~SVSqZbf}#9n@wh{xx? z{%^e^fEpr-r^dgV80MlU_v7$dsXj$$TRll!6Op0*uTE%c?L zv5nzPJz5ud|08YzAG4^31O?gX(11Q3R_~yf1779nqi?| z{Q@*Z9JAXDzboyqfl-bSK}35Dc?TqNnyti&D}AI1FO~Otp=S%N(UOJ1ej}2*J5^}H zf6rVAbxo@pdMpqPK-b2GV!x{`tm5#h0PFsC;NtJ97vs39cw}GyT=QO?zZL`7M`Wmh zB5_xvzr2^=99E@qK4MPI!G?P5Z-Uc!oj|q`h7qIbe3ClC^v%I}*zX<4!J$r`A&qVB z*~}}dquZDC(#jbR2S11az5A3B(*&M)-Q_hjk6ABi>#S4^ko?hY%wxITWY^-VgRX?qKCiixaPMn+_H;YAIH|*k6B2 z6w%G60fN?|6thG*R|+^McwbnKW|+HFPZmnMf*qDx@Z9&Oxgc`}jX|Qw#Nr~LIW{^1 zvm_7Z)AJ>nIWM`nF#YVc--YD&{g13O^TUzyl4jAo%?ov$dDSS%r5c8|zEJYfnd^FY zC&!5pxGP$!fSDzSasU)u=$fc7(Luo6N6wudbzzlB(VcpvlzDsr4;FA}4R&7yd}Z^? zaloh*pq8D7KnN!dt3>)Q7q$$<93cSAY}4+4#|;QJ+;hdB064o61t$t5qrt9Q3I|KA z1m?pj$xCs>#OM`+EyQ7=M7{~Pg-9M9g$v!+As*$aIEnag#Ll`y23v4Sc7o?)m-fPY z?hf%!_6%~L_?*WAsFYS-nwj^U-+Vmrf3j{8vhab}YFr{jiAwv1=lh0e5n@3m%Ae$r z`e6HqCK>=RiY9I;OcomR7G>p={p!YgH>*_2N8Av#`E)c*U!=t{EvcOyxOehNRJ4p} zk^BMDznHdY6XMM#_W=u{Wx3Mf-|T+<9hr>ot=a9x?&4RycAxr!4w^q8YP5v0eRzUP zqb*1VM`Fhf&MP$!xb(Rj7Aa%GoNSw(c6+Y($X?E6tosfEE(NgomOFS=o?NM|8{`|> z!G5%~Q;N1*f$xyqwlvWc`{pPPG0AYDZ*tg*@y{|D)&8SjLk6J(^`oRnr~4rzc% zc?F(cH_IkVK#v#YC*d!X+wvbgvfD3| z|0)KIxiOK4SYn6h1KC;5+ZlSse~{Erq{|rYwH8mIm6j5NKa<7?shu5-XBU{xAtQoj zb@tlW@SOIYdpIo){VRfiKiI%4j5{KB7o&-Uc^FpmPKNTxzu&SrtpUN+JG1=g;|%z7rWMoE>_Bz0CQn< z+vyinrRzR5ec(&yL3E-!A&(g)J7E!(GsczF;IeV;KtA>d=N_KTD^@(VzST>5g9L22 zc$E`jYPbnnHzeY@o#Dks;B}jq?W8)Sa8RYU9gBQ!Ws^DK+A3cpjiVvtgv-sY$T&0# zT~o#KqE8LnSkb5;efWph6D5rBX_7k|d)+}ejXpT%2kJYHMq$VyS$0+iIhVHNj4U~$ zwj93pqDr5=YpUR(KM0~mVBTHdGHW?{5ov<9JW7RIn)*)^%fpVBRWg(xvMzDeWH)^nd)~K0y(4z zxv2ycv|Ks-Q)YViip%I9OOshZulzVHMpi2qF31lj3z0P|^{k7_w$XLfjR^THu{Qb0+sqhAs;_J?s~`XQXW_HV!T8+Fs!&CA_y7M-KvK_THC(5@N7t!vy? z8=S_r+5)azZ(r;mHsY4)*xw4bnYYh17y;RH@!0&LQ~#m@2gDcZ%Q__kS-h@JfBMl! zas+j!wQG&P*y0g0&X%ExyFl68K9`_zRi*Axu9A!*L!=WCklJIO&KlHruQho&6wEE5 zA5$1~>f9W@FuSG5hDEgxt%Ya1&$qru1QOSDJmDe4%H8hzl+g-`>^xB8G6&uTvf2Mtii26~wT3v#63koc zf9Ij{rLV#M%n!@#dpxN;#aE>vcOSNIf((j!uRx{B>UW5FUH;g7{2=~wW1$JF+?(`5 zQu(XmL*uEC_GrrQ6TH==enWgrGlZpgmXzN6I}kB7*6&VtyM$SM?hIt0S23cZqF?lN z>#h*!?Nwx-G|zvkkhP2%LLM}%T2#=)&xp~9ZjkikQ$)JQeR)w1CnE7HaGCr7X(0i-2C;#`su1o?uYZqj;%jl>wtejMo03fKR+-@g zL6*6}h9-`_q4|aHX8`wNYT8({*SqK_-QnWq$B=^LEq#i2K3CkbPXco)WxIN2A zNFcYR(*bWdhv9f`VQ8OCOBCi~wt$Q3cxL8{&lP3(?SjOp-ZrFV-UR?z@6FEP7sK82 zH11_;#7azd64*nO$&an?dsL=jeZ-U8miJoBo7~%nS$cx6!H>kQ?W|$HUb@D&LClj1 z9#d?}$AhGTDg2G-*-p+jui7^)>Gi(^8AraNT)Xc6Dd4w$lotm=no^5EMC>L=G1F@{ z1_w3zCZnPlUQuJV%FK2kC&0GoGJuyYDBMr`JsgQtQC@mGCN@p8giZMO#4HyBOg`5% zRCc=EOJe$oqA`7?=+g<&V)qUg#BNQb(0^KXcm1vUFxm>;C$JlRPa@s59tJJz&zjV+ z?|3)2A28H@n17Wh+9Nd6?^mo;r<=~NnTV<}9WeR*RfGFgg-QXBZM_DnxLl2<$I^^I z-AK=Pr|A9XH$Ej&@0dCt_}e4vE8JOI{$Q6ocD(O0YNlPes?gTkAbgAL-3r+TOO`Gm zfRd|B@^gqRvN|#g-yD|+`lrf~`?ac)KsVG?*Z=xT9VZ4a(!ZTd+v%pN{Z1gDiyoPU zg{B*-xx@Fkh)I&_E8q7TD)-i&3?KSu@(b(K(^An8?^hAM?)}py@zja0GIOV|R6Ue6 zCl=Th52<&S**#5{>Er-ovYJ1}-@1I#1_pL}eKd!d0=j@3AHl0^jig!L8zr!r$UATncds;zo^*of2J-(ApR~nZ0&uF z*i{aY-*&D%kQ6hgE*?2J?TlHagC!j16ur! zk1s>#=Di@~3xfJ$JbH5{=;Jv-jk7ii=+r;YpP za`Dhtm)uzdbTOp2E<68f+IsjsYO6_x(AxpzGAUI3ZRv&IXv$Ug<8gYqP(FH(?P$xp z4gZwl&$`mfxq0VCBkIxWCA7$Ci5x;>%qtR>X&($BOk%}78Fh9*io9h8yY1GQ>x8fP zz%Xh(fN}iI_x;s2;R3T5_`G2@u+N$%tFcFa6o-1{GJN$0xi8K;;-j3&U4`f# zh9VORVtu&&TTAv2jm4D}W~9$(jETMeqW7sm<9v3!hZ|AibbG1Qiy4LrohP!j4V>T& z#U4ofZON&tm1Ib!)jwm~-`)%sR1+g!>y3O{t=NpvPbwhNAiYFiO12RnZnGq5 z*dL(3Ki3X6>4%>!O{3oJ$2t9C-z8a_62-#n5p>oGjJdp1ItYCj$4(R^F#k6EYqhZq z$FsZ#yq>KwMfSNq8vU-_;pd&i zsD;t!u+%*Y$>iaEF;l4@{pk(=8MbYbjA1NYFuA@650{YkTbKVP6OcfRNdB*$&N?cp zsBQbwHH725F520#A_fO(O_xDa^#yX4PWri{x zXMh(Ur~P9@P!HKRG27@N9FL(b&BvW?tiuHY`$RjG5 zz83!?_eT6MJXRy=w*+I%^h27twy2e%<*~XNU)hzc^?W{W_`6xaYhONmEAT*YnG3Aa7e3VCqoO4@a!sB#mSh0kt z8<+0Ph+!P|_Dzq59NIcj$b3B-?GaBP)EM>Cv8klk@Ok>xCI+(!csvPFF!QCToyq$Ou%xpR6J5#~HIR z2x82Z+9Me0pmT=bPxyi{0^2P)&qWk*|=Fyk~s&2p-cuH>gTH z;sT;K&oX>I314Q|fV=>G+Tv}Mx~1ynH8(LibhubsnHR+Fbc*9Fds{V#^7OFET19@s zSXm*_(b3)pK7)y`Q8F#Cpm$Zmxks$HmaZ2r#0ejxE55Z=iyVd#GBp?cXvxVp8Lc6i zZ^e;6y@L0?{Fd(!c)$;;MYjhTuVp?hQ4WTo~bd*jqKLbya zOmGlJaul)Sj9&xDVzN8e7oxQ}%L%DhG;SR)hFxrAd zKV0pf$mSn##IETd#YSE#KmXxOSO4;@9P1mQL8CYE=mKLSx3n5RGxxRse^a5{KSpLT_%uz2_?kr?srE-1VPiy$00DGKTQF^4o^h z3ZuJy{t}ePHn1*}W`Bgr70tfi3*HTKR(sJs!Ii6o7J@^uMtuNga77<4)JjTrW0gcZ zLEK>2f2PIb9C4ZpLsAcK3>~=!Z|hceJ7_M(;|YNg*J3L#WXjcW-;;;hG3p%L&E;i|#%y(~1yLsG~WlCg+zS3|V}) zhaonYPb92HV{ft$M^!0p(o0d+B)VsBz7m@%{HLge@0gaA5u@2$R*hW^wzW*yIB!vr zxy&_m8x&f-kU^#woUV=^@t}RQrN#RftXMXS_z82_2S{ms8%dH#UOEbSh#G|Vku*&) zo0Z(o_qaIklRhoZkf!-o`dHFP{>}^=Qm6Td?{*dPGA%8O_Bhpo2ii30eWfDS!l%@& zfYF%5LUg}~+AwXR$j;Da-hQlS_NQySmtq^ zh1Q;u9cZqIfrB9o!#%5gKh1t&o7t%mF2Yue2o#wo~3GAH+Kmj68*K zd?|7@RLFnXXCLYs^DI@KyNm7J-LiX%;NM9Z++3t>HA%PPi zTs#nU_TlLthoO+dy9&md;?X4Kh7@o(=V$=D4W_+&N~Bd26cYqn-*>!inIa_DY^(z# z+wpWK7QB88EA)jb5_b64eO{Hwi;%C{tm$$1&VLi$91YfbufHQ=`duk^^-BA#R|G@0 z?x~Fk*;1N0XF+N6LDOQChortNMdP}fkP+$DA$wvKgbhIwQ;qQt;#^+wwMzB(lQIQB z9>cZ~{+04qNX}gFHC$;B8!b8|MLjv(6eA@d>j&gKoyKVUqlO-7aYueBeL7|PAdi*z zmF0AIM|W7giSmmobc~lh;w`BKBS8cWXomt`DJ%H09ThVc9G6__-eC@dl zhcg(T2{Dlozmu+)W#Wb9Z)5syy|Wx+RL|xQ7fC0T)7Ndni<|2I2)j7CA`;FZgMSV8 znwh1h8;Kmawoty~7E~IPRT_)47i{NRzAp#4Bxco{?_y)U_0g^t(b?|?_5rHSeF0+k z#}mvD%c(`Ru)7p1-Bd#hV2_j&MY4ZCmX$rMHI@Y>?4Hy9jbLC-G07A@GZ$U3mCUp< zX4gq@#%YFNKIl0ne~h+Mr;Dp!!La|u3+ zS6v$Wiq*^`@NS$NC2=!UUP~e3m15CHnb}JwRaY{l2s-okhrIu-fEo<4vq(ylqlJHo z_2F~DvCM4oRB+ZoB(a?Zv%CJa<7OSJzfO{>`$^}v%$(P+6`TJqI59EF+?D}JT;|ze zCWTo4?IzIm2r@I79#kTp>XeX8{eZB)`24brB04K;l%CD1c)rdIT_L~eBHPU`Vz#ad z1guDyr`n+Pe)X0^L>FH~evVAsX(7RvIEMVpQ?8>t4wWL1Ao3k$O2Ww(AxcVHLqk6q z8e9t1+P*Mr-&BctGT^?ci=WHbX4817Pr=Z&p9DDkY8rmgs-;PWxm6$cZ!0jC!~~TS zzuw662#hz5N%4obh!5E6b|B4Q$uo+cx(*vvg`>xYSfOa%eM$9^8fmT4Xa)g`y@?{E z-`pk~5=+-{c*4TM3pHkpZew%(HP{*%nU#!vg|2#Hho@;sS~eqj4$>S+9$S=@b>jvb zfQI5wvSyp5N#~h@`hL{e_n260f%&NM2;D4q$vG<*oT9HA8GSEZzO`6kIQEjj2FGKV zpiLi`zHydl5xMhySRn5$?x3n%;w`wE17v$#`$yp~>}+U6Z8N0$IYcGEwC=)@5aR6V z9l`jxZdj}tRwo5QdMG~W_a8%7HJb_0$P&jD){6CI{xWcsrMyr8Jlu>m zt#m%+RZ;)#-drRMwuF*{!3R-DQC?g;o#&FHKGRQPRVWNXHi0INw4iG|AOn#kShIE5gJiS|go5uP2YS`K6U|Gv+${ zZo^W|`RgODZq3fVtFXTB;zTufOnibW#9W98rdS1F`k4V$Z!)Z>AL=6Z?WTsXx+t1iCydD!~UH3e{*V3o_+qf@fTX@h7Yw{S8 z!P#m59Y@vmzpO}^#WQ}2iut6YGttXeoJNvH^8K%ez_8EVN^4Ojr%JDs7Tja*OGLz5 zaQ`L#=1>6<3CZCRxN|{}Y{XJqFp}AJtVxsIc8*($s598a946};!eL5%d3l{A^Ca6R z`+N4S8e-Tcw}J@~Qw*VIE_F$)(3(36jU^J+_>H`FBE0P$4B4PcUKbOoSRElN4~_il zKaV;Yw`=?svgcY?2SM_DT`@a4Xnplywm|h7LwmIM?++{}u*G&*q*mY_C8Q0%qBkW4 z@kB6Qb@Y?a4Z1Z&VA|uco-fi{95TSN!n~Ux!N?dF@LTY0`?jXoVY$X!2T(}=DmXce zFyLHR>4n%WulG2zAn`q1QRxQdFVJ>$Qbngl6JV>8;>SRKL-1Q08F zFK@3HZDWCCeDLsx4W7HOLDClWv4+oMHJHS`3cpf;*~u(S2$E;@obXA^2v3fVs#LTj zzrI<3h>7ti>cg;MOv9R)>yy?G({C{XSWxDWtUePBl}~xS&02N`l(qtLNrrKyxV3qP z0Oo#Xa=@J&cRwjdY<$}9ukIFAMPTrnvH8e)2{;rp$D@rKHLI2{G7 z2dSVlqt#?Fogt%}Q5Y9QM5hhuxct0fn=x&dsqEE`W_4Fi(2^)5x1!qpCz0pm(Wniz zx4!luvud|+u}l`LTb6ysWNGTtEC4A&>x{~@V76zdb-@5G zw#`2i+G}Y^R7Th9zc9oP9lNC!j{*av;IXjg;_3eQJWNpCbb2W$43i`bDI%1s<}k<5 z<Kg3H=R=bJw2{C#p8Sn18T+c!8DWUrtlVb7VU#(Rsg;&1M5-Y zjdon71k-yg3nv+Ll1?-T&%@__ zyGHDM{P76m=9^&e*bYEA!3wa;xwd=QfI<>ccBikER{30 z_l`}Qb*Em|XoV*~X!=_ogx1<;9!ABb$Oz7loV(a=MDUbw4(d|`}_ShqF zxZCV)wkiR~m8|P;6eS;IT(B^#DhcGygBBgT`zHwy)+;(^D+q@fC6mnJ!mRTOo_#X2 z3V#>tD~wy5djI^pQTo=`ZPEsP}FPNhj$T0snV zzS=%_#!}Xkh-sKidERA3Fy>lTt~~#Yx9tJ8QpJWu=yleckO%ql$9?b}AugsqUFouv z5n-|~o9u{6HTpG|v+-@5& zX$YSWa)E<}=;MN+3^grveuh?H_oUBeF;hK|hMZn{|JTxx*l4Cid3i;>AZnV$^fZu3 zNQ*9LQH^j|ygm?w(uh9d!+q1{bGrE>5@0>o&u@O+WwWVa)>5;aEsdPYi1^O!m@>Tx z%%txjRrTanV_HEE$|La|qDdtp|2Cm!u8sZ2*?p=kNk{s(dZ#`e*}wZo94{m2`vCCE zC9s!C1@l^Z9WRM+TTRGxD3{lT6F?UCou`5r`GuTHV2=i78tA%m>f?T;&U%{eZk#MF z_g{2*wyodA9~5Q#Or^!PB>T8dXlSg8K8bcOiawL(W=({Z$kq=n0N2If(^yr$RCiA7riV1ltrs=2%LDzUb@m+5csS`bPbO zQu4@QS=qgm`y{F|oN>WBd=- zpXPIaV>$?;eP5TY*BjtlJ$V_L^a|W{2brgg{%UbZw69m)#HxMCD(RA34f+Rq#ns71 z#Cv4NnaACI4ZBnw)wj)J8&y5$##rlbj{V7t9$I5YCfvY*cn7#{9*;JffQg6OJGuU6 zV-*W4@rRcf0uwiN_C3ScuL`{L6tzFLf|Em>INsMUU;GQqpPY=V<8AyARMpg;6B2^k zpECL9=D&=)wFJ54!oNGXHs_yte@f2o`%_JX)o{caqVddpGx>XUrU&Y4r|w!~C;jLE zVfk1-Tyvi1lYfC|km;<){~nNKI{Ca1UF$BWPG#|T{1$A?pR}i2mK+(nd`>O)F)(5_=5-h|F1n$Uhinv3+4#~hu|Pb4+V-8NyL)1ZX85&| zy!`rnJ!lxAYU>b^MUGWoU@OaTY)D(?g=G0IfyMJA)fzCgWEOo8ok%2^J|sto_V+P72J5@25)sS2IHi3B(1g*SKX_m+#HDyM29q z>p}n{MO8u7deWh_mC5R?QS*_7C(vM2X5LHXm?za+cxASzu5+K{l1uW~?tJBTLg-h+ z4f%4+plrm4i)%5t#eCk;x~%X<4eJ7bZmcDxkAI2Ba7yl{*bNIU1f0lqesj`>kRamt ze-WzmKJhr3rSOk>VSz4p$10TkJ}>Zq)DQsPW2Z4ReW>_aWxeIdkZ8@OmF-HAb=sO? zHAquR@l)J9p042A<`B<=aIF7FgTXA-LkzI*ex*)~i$mtC57?ZK+nXFai;clqT$btDU72fm1wInDp^o2BrM!=mmBs80-6le?X;&vCYMw!?2QlmT64B>1P z3v-Gf_u12m(IdabrN(z^bb_yR9O`qNe~g9*><+%FI(9U^*;AwaTh!lV`k%oTMJ*G_ zTwZ=lV3b&5B7ZSFIJglh!n;gg3v_Q_nS9O0kvvx9TA95PG1Y$z?4lP^NwRJPGf$lb z*OPYb$6Dk2S(TFE{m4&ZW*6SnjFt-h>!b-nUGr zFN*bNsI1dPK-67y;QL>pd=2A+kE;aHju)m!*d9EV9jpVVFG_ojQTj1+0c(P|+F%K$ zow@uq@^}m74w#Y-``~;6E6kj@+{`{EE$t7R=6@EpIHSU}r?19$g_N19G#u(x>_x!B zU|+mz!lb?&ZUT`*`iDGaX1me=R5}V0Qj0jtp)pu)2|>+5Nn8jTmU}^mTh-PW2idn* zbo*r&KZnK41ChH4zh47!tnz1~m&WP&7orgk+qI*fY8|p}3WvYQa&1+L4KrMmMU+0+ zB}3*uWyP5s%U8i*mQJy;1(nG$9 z<J15M2*BYNN^p z!L?h2`v1WA|8ntcs9iss5FGT@$*7^2Dg_7?I0O?etj4Vnjc**r^_KDT7-Ta8%9m_b3926Z1440$fqza+h6ff#M6xKPE>vu? z6RBmNyNgZ`F76u;*K6|zNI?-DTI+`*RXUuXpo%U>ztxl9aRdFI;1Lu`o-XZ5Pws$ub=hQ2HYQp6ea%uWW@cC)6I=`;BwC zdIeqf3@>&@AHJS~8K{{mGpvx+fAXPuJi-XDkLHMq8v{%KZTVXs$!>sc%|@xURZ0k`Md=NaL|Y>1PT7gtxkBUzHKZRfb1kC)OUl|K01?J|JyTv%6B zuhnc-o~!}}G+8~`f|~ok#gGkKx5*g;O!sY5=Dm5yEK<8WtY0=>>G1!L;uA0tHj2jx~PFCF**77@DsST{4kvb9i3v1@kR?1F<3Ic)Giwk;z9Jqr{jP%;Lg>&f*VPm93~_T0M@QKU!Oaoj4TR zUpcs#Y5W@Jx|tgx)0Vi<8^&KJj(@E>LlwJ|$REd~$GOZ#R6qP>?4Bo=PhNyXZrh}2 zf9{Kly3`VOw2qt_>1+N}_@dBlN zqM?;n0x3gH|s1&u^ehc5@d z!r=#{+%_|30M?TGC6V_<`Bs+D=M%;ys35A^8`40^MSYm34ufJ^=V-f9_Pc9EbWBKx z&Z}PIW&ifiyBW(K?V~0P>jf0;tCyl2S#o)pE7m8A(-sqAdSpUs(@xzyKYCMTmCh4N zyH`#A`nAELjx5E0$ER19C52{ZqY@4(zw%79 z8JL`N=kbNV@0Gi&pIE^3N(F_ypbI2|Mu{fdNnTPhwtk~yuzkoLr(au8tQ%A7_EGr| zbgWrH4krA#>;duGuk~)S)N02`_pGYyxxBo7Q-Fx@efrrmlCys=IT)?}3wwWOL3r0P zMn?h%l!vQs`E2_gdRh(K#8x|mncIV1wB%}Tlvy^2XdU_ z*_VblG6MLJ#l^)R)&o{?*c^H2@C!7ztB1UqMoW}p&GJx#>lYn==G|aF3^=-bdvYx2 zhb*RU9K6p~WlVqY2s>}^>!i4%vpg>bL;tRThz1hzsG^dK`xnX22hEZz+xuP9J8PTP zH+HM{Roj%)2G4qD zS=EVUXdN1~3T(5lJuG`+?tI!GZvL?s@(0SfqETZ#`2zPPrSXo}`sh~Crw6z2go;gI zN*d=LbZ#y?T?(yV`B~SK|3}}?n$IrFB|KlN){bkriInq1BaTA{uH$d|WsGVfT5`#c zy!v6s4gZBDe*5DS!xH7~Yf(xr$m4y>SK0BcH6wNIkCmsIrHi45`w6R{d^*dq=d6Xw z*2P=UhZ5)Ml5cOasJ~s-X1S4p1datHly$=Yym=L+&0TS|6E?Zex89{9dD6Y^;;=cV z@IGCXWSd=ob-8%=8>V*ZTLoGuD(VeaNz3a`z>_Qjw70w0=wU*e-}?etSVZLH;P1!v zz-asRU#Dt`H?uan?H(!nHgmbSw+RvLe{V?!+HdiZ;68rq3`#lPjh|bls)clb?a!=w zmq?<%_E4+I(%va{c-gS-!}&2#s9lDs?|nq-ym!OBESL7${&h7B_mszatWZ=Mm)wu* zz=PYl6*OHq$(y|!umfEIP~DCe-~AJBM~bj~n=bjY>}5M*!EIN}nrWrZXm)6yn z^O2aF1BQtAXJ@ks27yngBP0XYGj{KZ@3$7$FS9J>!A=btbofiY8_85;yLxMmymLDW zhxo|zUAT$q*vFXNfEFB}<%K;${Md4I#KC0jp>U~wJIauvBJ}!62$<<=xDjr^I$mMa2%Gk!+)b6+xmv;X?1I%d3esNV%h4x{{1&a z+?3huzk;F*AFuBMC;wWlHTUd!WetbvsSggOCP&$df-zGbwV3T<8js6K00xw3FFJqm zlDYO(8I@dh{O&E%m8hvu{ipgv!aGPxzW9LFc$3Gg#Za3df1s-`Wy4%LsiN(vo|GN) zj3c`vOnxxohwO9%luIm*;kUzfI>f1QBlf6Bchm+jtMJ4Y?&I3$wdK+Z{pJ$EQ=phO z^+&C2ueF$eI8`7A^B~2(ji+RMD{(AaOT~^+HEz3hT9<8g_`+s2qg(s@{aWevpFezy znNzRz>5zs`T7jItOOI!_a8H1{A#UMLikr_ zwfL&<^7y6oWK>J%ChGdW(QD0*(mW>*B0sB|yZ67w^O$jDp77cJImoQCJ;cIvcYCUB zyVagb1?iW$!z-k&gGhi@YKw4tW9+kOm_`Va+50a;A{3{mjf{I#Ic zTTZcS(Ffvrda(tT=l?pVc>E3gcF|HGl2B+~_t5I1F1zgDr9I2A96Ea1XsKNPT{fMY zlN`!UymB^%)Y%c$sPS-qs7uK|1ss0=OiYhcF+-^!)cz{o#kMeQCTEWLPW~;Nsw_-b1Y5$|`$3{S` z_4a~?d{C{cmqmu4@)@Lpz~qzUov_oFpye4B#)pgGBz-d@GR6s`wLer*7FgyX2(WzG z$Z5h(6vKwU*iN5^rA5Xy_PP0TC1LMRYVL6qM;;tlv~fG|zIh#1sXHAV2#mu7r@UFi zo=%qjwKxKBDJobD1rd-J3bvv||B#jqXr71JqccJ-(7>UKl`lA?^6v zo6AZ%AY%dihE zvCGfgT&$%|JyU%4@PuU#enX%E*cLobW|ZoHa6rrg5NRJKOO8iMM{0B;p%R25n<$Xe zt=I4qnNzaGz>$$q2Q>xwU>#h)+3ZLk)sE(8<8i69juBFJ%x zV}b{}N49U|%bs=W7JV(W)xpJk{b}WON(?6dqDxitr9(lS>SQYW& zA=lkX;exBrPyAEl^G1_(Raq8VGykj%%8q$AI1*gxV$Y^YUS^!MgAV=!32yy;a1#A_ z6Y4P0dh%zt&Ao`Kt)|VqGZKo*#ucRU7EG4bHs4C) z1`pg~37GF_a=$Lio>~Tj#H`AC8;?ENHggv&SxaS1rH(gQEdgLQ*jif8w z9cB5^07_5U+V4rQ)7TS5-imbbEqTAgt>j8o=-s}4S2uxIWCGk0;LNeae|xsFkxN*; z)}vr$c^oWLAqKXVT?Y?~z zmJE9j{WI?}iT7$hvB|>F%st;Vf=n+;D8P)5I`tgvOdm2mzIZcHL?@yH*$ymcujq{uPvBKQ&nW2$n zXStd=h;Vl{sZgj?0JVIi{*> zpATXUXj%_dGwx=69xZAU=+cZKamHP{+IU8)Fz?KJOyOlvcHKI!DKD@OBz3Mevc>OX zZJ^t;sC{I8u*@%IoVZ0)+<>%poXp%)k^M}5(a`7RN*v{-;{<2y#EhY3$f%c>z;?%u ztldTf;JRf{ZpUox%1`Tm=|(v?xoFvkub;sQT~|&sUsUp+LO^gA?GiE&&t|eG%b$Nx z222>^mZPqy3`OG+00xQDU}tt;XB*#(ro0Qoki1VKR@f`?mu#aYsASX4`x~sg9^|{ z@JM{+#jLKA-GeWXQ<~d3!Ug)v^!6E*TKz;ZkR?SV@r|;z@YeBj9@v>yFp;}fW6TYz z9^S@M_cBP+EC+h;%d@u@?)-aDG@B46fS#};@p9qi`TMa{8zTbN5r*AB zb$5q7&5&T-SjGh9^A9nNl@piAEez%|=!czT8kcz4@0o~K>(}RM$CQnX$o&m}-5b%a zCP;-IA(2qZ}r7BgX8NnQNDR4;W$O+8w(@NDfFW`mTSv+lvjDa z9hIFWb4vJ{mLoPhQn~Z<#kh|`3US0FS1xrJ*X6Ex2KmvGvDjK?Gq_;jA|rJfZih** zNJI$u(@d6+_u_Kp^I zk`v3=FxuyA*Sv{TC31*bMbTHbM4#^A3tc0%ZE)`eQ=tQ@hT#W2z$LFyg4rbeEK}m> z6@u`L2@HITTn~g0fxmHn;rCu&hHuRAhxpu`*N-l-iO3U3}sm49L!T$5N1d>7W!Y}5Jmb4F8IMtn!bO9VdX zn(eF+o$?H=GHqwnTVdL_)D@o&rcVq68V`nB1qLiX&eYZWWCX-KBU&6c7p-8zEf<~n z@ApkRfBay}^`T-x_)qa~k*8KNl=(^y$8vmrBwqt~WWasKixNKI+ot}k-&%jsuk(h$kk}B){ zOW&@>n&b>{Sxhn)7nzqhx_mz7f_mKoavto$p}_*BSOcS@>s0#G($G*V&Gw4S?vzUr zz&}k*m4uzggyBtzx+&pDu$6{%$_;dQ_)y1P!g86T;|{6@n@PcH6@F@(*?LRYLMbbqFXX__ zh^VpPR~iF*_mWCmDJh54avc7-h{;C49?mLru+a0D78V)MAQKupcpS;wUyn7Rp~jeS z;72Jir*EW2|_)3yCRYl@`_XNLsPfQv=u|(jO(Z%6|;k-Z1w%*bbN>3-|ce3 zE_=$(twYhTel%Wxj949kt@@({HQk464b)ydK@2hI2u;4(lrQGhQ`9qLS4A2r!8f*z zq;%@0EeRj4agJI%T})T2+9@;vL`}qYY>Xge_z<1#(c!4=Gx`{`Ad2qBuI-)<94utR zU#*-o^fzAYxrlbLeUR6uk0I7N#0^s4um%Q&-bFIm5@RlBsRVN01uako<{`L3JMhXS znqKh7*)VdmPl0}auu`oo?{d;@jBtl9{JP+3A#sN_8HWVlJT5A`E`|Y&)NGN&Q`GH8 z*!qhv8@zTWEuYaxIMYZR(UZ(rZr12L*~(;8?ECpUA3X?^^#-4AgT{eliqC7@&r;s4 zw5--)BTKTMH>b!ZG_?h};-BhV3!INnFs_v|5AwiB#1T-Vg|Be}Ti;CB<9Rg;6+AmT zf)#-`Kowb^LOXyb2?t2EQbFd-N2Rfp{rTea`)d48f&C@1^3f6<mIyqR1CA6Dms<^knBfZ(X9%Q{6HL zPa)qZ|5DEb=e{+4{n*f-$pLPOoE(4Nr}D}-Dq5;-Ts}?f`e^W0xw{QTeOK~b6l%I8 zJ(!IS7O@YB2mIp2Zse(HVb1z!luBoshqMDW1 zNtFv#rOI4-yV4=_3U{{X1F+ean}(m0jQ5#@hs#XghV|6flB6p=>uJMa6O)I@X*j*_rrKRe1| zBuQ8&&@Y5R`Q7!MTX#6H(=6B5r{BK(ywur&w-s^mwz^6*0F5N9T@2=2(Y#K2_F@#d z!7&LRX;ci_UYji6P8d3KY5xYye}jivEH*H;aqrV={FY7Ur&?J~VUVf2|#1G2f{_ z!h!ybmy=n-hHpHzG?_h6uj;ueDeys!gKk`osRxhWO4xV8QWayklc+q>gjm0IFL*?Y*0Xv;KcR{-43cUi>n|rLLgT@Ph5!j1I ztX=BW4ajY4kNgK~C1Lb0G~0RDa@5eZPWRUxUSLli$j2apIPER1OtJ-N>UIi7j%dZ3 z!+0(Bok0SKPPU+nKj@bq7VXIC;#y5Gu7XB?bIkSS{>x}8*oBVU8<^>`XX_gyz->E@ zBoD8zpHvec7Z|Vb3ZcSiMd|u1AnQu0iTH$%jsfZ4R`75wU*e)|m%2v76rdy+$-Yjb zLh@Rf#O&kJr6U&Z!6aNhm^)9_r^9cy-#+kDb`>h5m96iGj^d zI9j#nqFDa$`$=Lg^;CSGLY)HmmZwwlJKv5G`$Z>2$%^X&d3duE=3BlaM6&iH3Svmt zX3dO;7RJWENhIsuya7q`WAb)Wv-pXJBILkV4P{~|o?vsG0HwOhSox)FB68vYDL(6Db2JAPJqv)7xAizodw zHOIZB=xY;;j2iqO_&2q5QRc9RS_V5{S+3)MB{P%Ou=7VItz~=P|2|paV?n@zx6kHP z6V!uO{VR-&7+Nq^Mp@dq **Note:** Replace `recipient@example.com` with the email address used in your seed test data (`cust-001` record). + > **Note:** In sandbox mode, SES requires `ses:SendEmail` permission for both the sender and recipient identities. If your SES account has **production access** enabled, recipient verification and permissions are not required. 4. **Note your SES identity ARN** — you will need it during deployment: @@ -49,7 +51,7 @@ This pattern sends emails through Amazon SES. You **must** have a verified SES i ```bash aws ses get-identity-verification-attributes \ - --identities noreply@yourdomain.com rajilpaloth@gmail.com + --identities noreply@yourdomain.com recipient@example.com ``` ### Prerequisites — DynamoDB Table Population @@ -80,20 +82,12 @@ The notification processor Lambda in this pattern only **reads** the table and * ![Architecture diagram](Architecture.png) -EventBridge Scheduler (rate 1 hour) -│ -├── on failure ──▶ SQS DLQ -│ -▼ -Notification Processor Lambda -│ -├── READ ──▶ DynamoDB (abandoned-carts) -│ query CartAbandoned = "true" -│ filter NotificationSent = "false" -│ -└── SEND ──▶ Amazon SES -per-customer abandoned cart email -then mark NotificationSent = "true" +The numbered steps below correspond to the labels in the architecture diagram: + +1. **Amazon EventBridge Scheduler** fires on the configured schedule (default: every hour) and invokes the Notification Processor Lambda function. +2. If the scheduler fails to invoke the Lambda after 3 retries, the event is routed to an **SQS Dead-Letter Queue (DLQ)** for investigation. +3. The **Notification Processor Lambda** queries the **DynamoDB Global Secondary Index** (`CartAbandonedIndex`) for all records where `CartAbandoned = "true"` and filters for `NotificationSent = "false"`. +4. For each eligible record, the Lambda builds a personalised HTML email and sends it via **Amazon SES**, then updates the DynamoDB record to set `NotificationSent = "true"` with a `NotifiedAt` timestamp, ensuring no duplicate emails are sent. ## Deployment Instructions @@ -145,27 +139,29 @@ then mark NotificationSent = "true" terraform output ``` -## How it works +## Verify Deployment -1. **EventBridge Scheduler** fires on the configured schedule (default: every hour) and invokes the **Notification Processor Lambda** function. +After `terraform apply` completes, verify the resources were created: -2. The Lambda function queries the **DynamoDB Global Secondary Index** (`CartAbandonedIndex`) to retrieve all records where `CartAbandoned = "true"`. +1. **Confirm the EventBridge schedule is active:** -3. For each abandoned cart record, the function checks the `NotificationSent` attribute: - - If `"true"` → the customer has already been emailed, so the record is **skipped**. - - If `"false"` → the function proceeds to send an email. + ```bash + aws scheduler get-schedule --name $(terraform output -raw schedule_name) + ``` -4. The function builds a **personalised HTML email** containing the customer's name, cart items, cart total, and a call-to-action button, then sends it via **Amazon SES**. +2. **Confirm the DynamoDB table has seed data:** -5. After a successful send, the function **updates the DynamoDB record**, setting `NotificationSent = "true"` and recording a `NotifiedAt` timestamp. This ensures the customer is **never emailed twice** for the same abandoned cart, even if the scheduler fires again. + ```bash + aws dynamodb scan --table-name $(terraform output -raw dynamodb_table_name) --select COUNT + ``` -6. If the scheduler fails to invoke the Lambda after 3 retries, the event is sent to the **SQS Dead-Letter Queue** for investigation. + Expected: `Count: 3` -**Seed test data included:** +## Seed Test Data | CustomerId | Email | CartAbandoned | NotificationSent | Expected Behaviour | |---|---|---|---|---| -| `cust-001` | `rajilpaloth@gmail.com` | `true` | `false` | ✅ Will receive email | +| `cust-001` | `recipient@example.com` | `true` | `false` | ✅ Will receive email | | `cust-002` | `activecustomer@example.com` | `false` | `false` | ⏭️ Not abandoned — not queried | | `cust-003` | `alreadynotified@example.com` | `true` | `true` | ⏭️ Already notified — skipped | @@ -175,7 +171,7 @@ then mark NotificationSent = "true" ```bash aws lambda invoke \ - --function-name cartnotify-notification-processor \ + --function-name -notification-processor \ --cli-binary-format raw-in-base64-out \ --payload '{ "source": "manual-test", @@ -193,7 +189,7 @@ then mark NotificationSent = "true" } ``` -2. **Check the recipient inbox** (`rajilpaloth@gmail.com`) for the abandoned cart email. Check the spam/junk folder if it does not appear in the inbox. +2. **Check the recipient inbox** for the abandoned cart email. Check the spam/junk folder if it does not appear in the inbox. 3. **Verify the DynamoDB record was updated:** @@ -264,7 +260,7 @@ then mark NotificationSent = "true" | Symptom | Cause | Fix | |---|---|---| -| `MessageRejected: Email address is not verified` | SES sandbox — recipient not verified | Run `aws ses verify-email-identity --email-address rajilpaloth@gmail.com` and click the verification link | +| `MessageRejected: Email address is not verified` | SES sandbox — recipient not verified | Run `aws ses verify-email-identity --email-address recipient@example.com` and click the verification link | | `AccessDenied` on `ses:SendEmail` | SES identity ARN mismatch | Verify the `ses_identity_arn` variable matches your sender email exactly | | Lambda returns `notificationsSent: 0` | All abandoned carts already notified | Reset with the `update-item` command in the Testing section | | No items returned from GSI query | GSI not yet backfilled | Wait 30 seconds after deploy for the GSI to populate | @@ -295,7 +291,7 @@ then mark NotificationSent = "true" ```bash aws ses delete-verified-email-identity --email-address noreply@yourdomain.com - aws ses delete-verified-email-identity --email-address rajilpaloth@gmail.com + aws ses delete-verified-email-identity --email-address recipient@example.com ``` ---- diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json index 230b6bc6fb..725af95c09 100644 --- a/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/example-pattern.json @@ -1,6 +1,6 @@ { - "title": "EventBridge Scheduler + SES Integration- Per-customer notification scheduling (abandoned cart, billing reminders)", - "description": "Create a EventBridge scheduler which sends per customer notifcations for abandoned carts and billing reminders using SES.", + "title": "Amazon EventBridge Scheduler to Amazon SES - Per-customer notification scheduling", + "description": "Create an Amazon EventBridge Scheduler schedule that sends per-customer notifications for abandoned carts using Amazon SES.", "language": "Python", "level": "300", "framework": "Terraform", diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf b/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf index d26462f2c5..2bb200f7c0 100644 --- a/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf +++ b/eventbridge-scheduler-ses-abandoned-cart-notification/main.tf @@ -110,6 +110,10 @@ resource "aws_dynamodb_table" "abandoned_carts" { tags = { Project = "${var.prefix}-abandoned-cart-notifications" } + + server_side_encryption { + enabled = true + } } # ── Seed test data ── @@ -120,8 +124,8 @@ resource "aws_dynamodb_table_item" "test_user_abandoned" { item = jsonencode({ CustomerId = { S = "cust-001" } - Email = { S = "rajilpaloth@gmail.com" } - CustomerName = { S = "Rajil Paloth" } + Email = { S = "recipient@example.com" } + CustomerName = { S = "Jane Doe" } CartAbandoned = { S = "true" } NotificationSent = { S = "false" } CartItems = { L = [ @@ -272,8 +276,9 @@ resource "aws_lambda_function" "processor" { ############################################################ resource "aws_sqs_queue" "scheduler_dlq" { - name = "${var.prefix}-cart-notify-dlq" - message_retention_seconds = 1209600 # 14 days + name = "${var.prefix}-cart-notify-dlq" + message_retention_seconds = 1209600 # 14 days + sqs_managed_sse_enabled = true } ############################################################ diff --git a/eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.zip b/eventbridge-scheduler-ses-abandoned-cart-notification/notification_processor.zip new file mode 100644 index 0000000000000000000000000000000000000000..9d8256b4db430cb9af5cfd65e7c4c5e5e0810f4c GIT binary patch literal 3047 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 literal 0 HcmV?d00001