mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-03 06:53:17 +00:00
chore(meals): remove email_report payroll SES path (PLAT-135)
Stop Monday SES deduction emails now that Flex checkcomponents owns payroll posting.
This commit is contained in:
parent
aceb174c4b
commit
fa0ef6b567
11 changed files with 54 additions and 145 deletions
33
README.md
33
README.md
|
|
@ -18,16 +18,15 @@ Monday 7:30am ET Employees (Mon–Thu) Thursda
|
||||||
│ - Slack notify │ └──────────────────┘ │ │ - Slack summary │
|
│ - Slack notify │ └──────────────────┘ │ │ - Slack summary │
|
||||||
└─────────────────┘ ▼ └──────────────────┘
|
└─────────────────┘ ▼ └──────────────────┘
|
||||||
┌──────────┐
|
┌──────────┐
|
||||||
Monday 7am ET │ API GW + │
|
Thu 10am: Slack DM │ API GW + │
|
||||||
┌──────────────────┐ │ Lambda │
|
reminders to employees │ Lambda │
|
||||||
│ EventBridge │ │ submit │
|
who haven't ordered │ submit │
|
||||||
│ - Email payroll │ └────┬─────┘
|
└────┬─────┘
|
||||||
│ deductions │ ▼
|
▼
|
||||||
└──────────────────┘ ┌──────────┐
|
┌──────────┐
|
||||||
│ DynamoDB │
|
│ DynamoDB │
|
||||||
Thu 10am: Slack DM │ orders │
|
│ orders │
|
||||||
reminders to employees └──────────┘
|
└──────────┘
|
||||||
who haven't ordered
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Form frontend decisions
|
### Form frontend decisions
|
||||||
|
|
@ -44,7 +43,6 @@ the generated HTML, and the generated deployment artifact remains self-contained
|
||||||
| When | What | How |
|
| When | What | How |
|
||||||
|------|------|-----|
|
|------|------|-----|
|
||||||
| Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB |
|
| Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB |
|
||||||
| Monday 7am ET | Email previous week's payroll deductions to `payroll@` | EventBridge → Lambda → SES |
|
|
||||||
| Monday 7:30am ET | Scrape menu, generate form, upload to S3, post link to Slack | GitHub Actions cron |
|
| Monday 7:30am ET | Scrape menu, generate form, upload to S3, post link to Slack | GitHub Actions cron |
|
||||||
| Mon–Thu | Employees visit `orders.seahaven.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB |
|
| Mon–Thu | Employees visit `orders.seahaven.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB |
|
||||||
| Thursday 10am ET | DM employees who haven't ordered yet | EventBridge → Lambda → Slack DM |
|
| Thursday 10am ET | DM employees who haven't ordered yet | EventBridge → Lambda → Slack DM |
|
||||||
|
|
@ -85,11 +83,10 @@ Stack name: `meal-order-manager` (us-east-1)
|
||||||
- **CloudFront** — HTTPS distribution with custom domain `orders.seahaven.com`
|
- **CloudFront** — HTTPS distribution with custom domain `orders.seahaven.com`
|
||||||
- **DynamoDB** — `meal-order-manager-orders` (orders, menu, roster, config)
|
- **DynamoDB** — `meal-order-manager-orders` (orders, menu, roster, config)
|
||||||
- **API Gateway** — HttpApi for order submission and admin operations
|
- **API Gateway** — HttpApi for order submission and admin operations
|
||||||
- **Lambda** — 7 functions: submit-order, admin-authorizer, close-form, aggregate-orders, slack-notifier, sync-roster, email-report
|
- **Lambda** — 6 functions: submit-order, admin-authorizer, close-form, aggregate-orders, slack-notifier, sync-roster
|
||||||
- **EventBridge** — scheduled rules (dual EST/EDT) for close, reminders, payroll email
|
- **EventBridge** — scheduled rules (dual EST/EDT) for close, reminders, roster sync
|
||||||
- **Secrets Manager** — Slack bot token
|
- **Secrets Manager** — Slack bot token
|
||||||
- Migration note: the existing `meal-order-manager/form-api-key` secret remains until this change is deployed and verified, then must be deleted during post-deploy cleanup.
|
- Migration note: the existing `meal-order-manager/form-api-key` secret remains until this change is deployed and verified, then must be deleted during post-deploy cleanup.
|
||||||
- **SES** — payroll deduction emails
|
|
||||||
- **CloudWatch Alarms** — coverage across the stack, all notifying the shared `site-alerts` SNS topic (see Monitoring)
|
- **CloudWatch Alarms** — coverage across the stack, all notifying the shared `site-alerts` SNS topic (see Monitoring)
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
@ -99,7 +96,7 @@ CloudWatch alarms are defined in `template.yaml`. Every alarm sends to the share
|
||||||
OKActions, and treats missing data as not breaching (so idle/cron functions don't
|
OKActions, and treats missing data as not breaching (so idle/cron functions don't
|
||||||
sit in ALARM between runs). Alarm names follow `meal-order-manager-<fn>-<signal>`.
|
sit in ALARM between runs). Alarm names follow `meal-order-manager-<fn>-<signal>`.
|
||||||
|
|
||||||
- **Lambda Errors / Throttles** — one alarm each per function (7 functions), Sum
|
- **Lambda Errors / Throttles** — one alarm each per function (6 functions), Sum
|
||||||
over 5 min, fires on any error/throttle (threshold 0).
|
over 5 min, fires on any error/throttle (threshold 0).
|
||||||
- **Lambda Duration** — p99 over 5 min at ~80% of each function's timeout.
|
- **Lambda Duration** — p99 over 5 min at ~80% of each function's timeout.
|
||||||
API-fronted functions (submit-order, admin-authorizer) evaluate 3/3 datapoints;
|
API-fronted functions (submit-order, admin-authorizer) evaluate 3/3 datapoints;
|
||||||
|
|
@ -168,9 +165,8 @@ sam deploy
|
||||||
1. Create the Slack bot token secret: `aws secretsmanager create-secret --name meal-order-manager/slack-bot-token --secret-string "xoxb-..."`
|
1. Create the Slack bot token secret: `aws secretsmanager create-secret --name meal-order-manager/slack-bot-token --secret-string "xoxb-..."`
|
||||||
2. Set the Google OAuth client ID: `aws ssm put-parameter --name /meal-order-manager/google-client-id --type String --value "<YOUR_GOOGLE_CLIENT_ID>" --overwrite`
|
2. Set the Google OAuth client ID: `aws ssm put-parameter --name /meal-order-manager/google-client-id --type String --value "<YOUR_GOOGLE_CLIENT_ID>" --overwrite`
|
||||||
3. Update the Slack channel SSM parameter: `aws ssm put-parameter --name /meal-order-manager/slack-channel-id --value "C0XXXXXXX" --overwrite`
|
3. Update the Slack channel SSM parameter: `aws ssm put-parameter --name /meal-order-manager/slack-channel-id --value "C0XXXXXXX" --overwrite`
|
||||||
4. Verify SES sender identity for `adam@seahavenind.com`
|
4. Set up DNS: CNAME `orders.seahaven.com` → CloudFront distribution domain
|
||||||
5. Set up DNS: CNAME `orders.seahaven.com` → CloudFront distribution domain
|
5. Roster syncs automatically from Slack channel members (runs Monday 6:55am ET), or seed manually: `python3 scripts/seed_roster.py`
|
||||||
6. Roster syncs automatically from Slack channel members (runs Monday 6:55am ET), or seed manually: `python3 scripts/seed_roster.py`
|
|
||||||
|
|
||||||
## Local Workflow (no AWS)
|
## Local Workflow (no AWS)
|
||||||
|
|
||||||
|
|
@ -210,8 +206,7 @@ meal-order-manager/
|
||||||
│ ├── close_form/
|
│ ├── close_form/
|
||||||
│ ├── aggregate_orders/
|
│ ├── aggregate_orders/
|
||||||
│ ├── slack_notifier/
|
│ ├── slack_notifier/
|
||||||
│ ├── sync_roster/
|
│ └── sync_roster/
|
||||||
│ └── email_report/
|
|
||||||
├── scripts/ # CI/CD helper scripts
|
├── scripts/ # CI/CD helper scripts
|
||||||
│ ├── upload_menu.py
|
│ ├── upload_menu.py
|
||||||
│ ├── notify_slack.py
|
│ ├── notify_slack.py
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,6 @@ locals {
|
||||||
duration_timeout = "60s"
|
duration_timeout = "60s"
|
||||||
duration_datapoints = 1
|
duration_datapoints = 1
|
||||||
}
|
}
|
||||||
"email-report" = {
|
|
||||||
function_name = aws_lambda_function.email_report.function_name
|
|
||||||
duration_threshold = 24000
|
|
||||||
duration_timeout = "30s"
|
|
||||||
duration_datapoints = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ FUNCTIONS=(
|
||||||
admin_authorizer
|
admin_authorizer
|
||||||
aggregate_orders
|
aggregate_orders
|
||||||
close_form
|
close_form
|
||||||
email_report
|
|
||||||
slack_notifier
|
slack_notifier
|
||||||
submit_order
|
submit_order
|
||||||
sync_roster
|
sync_roster
|
||||||
|
|
|
||||||
|
|
@ -53,20 +53,6 @@ locals {
|
||||||
function_name = aws_lambda_function.sync_roster.function_name
|
function_name = aws_lambda_function.sync_roster.function_name
|
||||||
input = null
|
input = null
|
||||||
}
|
}
|
||||||
"payroll-email-est" = {
|
|
||||||
description = "Email payroll deductions Monday 7am EST (12:00 UTC)"
|
|
||||||
schedule = "cron(0 12 ? * MON *)"
|
|
||||||
function_arn = aws_lambda_function.email_report.arn
|
|
||||||
function_name = aws_lambda_function.email_report.function_name
|
|
||||||
input = null
|
|
||||||
}
|
|
||||||
"payroll-email-edt" = {
|
|
||||||
description = "Email payroll deductions Monday 7am EDT (11:00 UTC)"
|
|
||||||
schedule = "cron(0 11 ? * MON *)"
|
|
||||||
function_arn = aws_lambda_function.email_report.arn
|
|
||||||
function_name = aws_lambda_function.email_report.function_name
|
|
||||||
input = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Execution roles for the seven Lambda functions plus the API Gateway role that
|
# Execution roles for the six Lambda functions plus the API Gateway role that
|
||||||
# invokes the admin authorizer.
|
# invokes the admin authorizer.
|
||||||
#
|
#
|
||||||
# Every Lambda execution role is created under the /tf-managed/ path and
|
# Every Lambda execution role is created under the /tf-managed/ path and
|
||||||
|
|
@ -325,55 +325,3 @@ resource "aws_iam_role_policy" "sync_roster" {
|
||||||
role = aws_iam_role.sync_roster.id
|
role = aws_iam_role.sync_roster.id
|
||||||
policy = data.aws_iam_policy_document.sync_roster.json
|
policy = data.aws_iam_policy_document.sync_roster.json
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# email-report
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "aws_iam_role" "email_report" {
|
|
||||||
name = "${local.project}-email-report"
|
|
||||||
path = "/tf-managed/"
|
|
||||||
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
|
|
||||||
permissions_boundary = local.boundary_arn
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "email_report_basic" {
|
|
||||||
role = aws_iam_role.email_report.name
|
|
||||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
||||||
}
|
|
||||||
|
|
||||||
data "aws_iam_policy_document" "email_report" {
|
|
||||||
source_policy_documents = [data.aws_iam_policy_document.dynamodb_read.json]
|
|
||||||
|
|
||||||
statement {
|
|
||||||
sid = "ReportsBucketRead"
|
|
||||||
effect = "Allow"
|
|
||||||
actions = ["s3:GetObject", "s3:GetObjectVersion"]
|
|
||||||
resources = ["${aws_s3_bucket.reports.arn}/*"]
|
|
||||||
}
|
|
||||||
|
|
||||||
statement {
|
|
||||||
sid = "ReportsBucketList"
|
|
||||||
effect = "Allow"
|
|
||||||
actions = ["s3:GetBucketLocation", "s3:ListBucket"]
|
|
||||||
resources = [aws_s3_bucket.reports.arn]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Scope SendRawEmail to the verified sender domain/identity rather than "*".
|
|
||||||
# SES still enforces verification; IAM pins the From identity ARNs.
|
|
||||||
statement {
|
|
||||||
sid = "SendPayrollReport"
|
|
||||||
effect = "Allow"
|
|
||||||
actions = ["ses:SendRawEmail"]
|
|
||||||
resources = [
|
|
||||||
"arn:aws:ses:${var.aws_region}:${local.account_id}:identity/${var.sender_email}",
|
|
||||||
"arn:aws:ses:${var.aws_region}:${local.account_id}:identity/seahavenind.com",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy" "email_report" {
|
|
||||||
name = "email-report"
|
|
||||||
role = aws_iam_role.email_report.id
|
|
||||||
policy = data.aws_iam_policy_document.email_report.json
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# The shared layer and the seven functions.
|
# The shared layer and the six functions.
|
||||||
#
|
#
|
||||||
# Packages come from the artifacts bucket (see artifacts.tf). Function packages
|
# Packages come from the artifacts bucket (see artifacts.tf). Function packages
|
||||||
# contain the handler only: boto3 comes from the runtime, and fpdf2 plus the
|
# contain the handler only: boto3 comes from the runtime, and fpdf2 plus the
|
||||||
|
|
@ -205,36 +205,3 @@ resource "aws_lambda_function" "sync_roster" {
|
||||||
aws_iam_role_policy_attachment.sync_roster_basic,
|
aws_iam_role_policy_attachment.sync_roster_basic,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# email-report — emails the weekly payroll deduction report
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "aws_lambda_function" "email_report" {
|
|
||||||
function_name = "${local.project}-email-report"
|
|
||||||
role = aws_iam_role.email_report.arn
|
|
||||||
handler = "handler.lambda_handler"
|
|
||||||
runtime = "python3.12"
|
|
||||||
architectures = ["arm64"]
|
|
||||||
memory_size = 128
|
|
||||||
timeout = 30
|
|
||||||
|
|
||||||
s3_bucket = aws_s3_bucket.artifacts.id
|
|
||||||
s3_key = aws_s3_object.function["email_report"].key
|
|
||||||
source_code_hash = data.archive_file.function["email_report"].output_base64sha256
|
|
||||||
|
|
||||||
layers = [aws_lambda_layer_version.shared.arn]
|
|
||||||
|
|
||||||
environment {
|
|
||||||
variables = merge(local.common_env, {
|
|
||||||
PAYROLL_EMAIL = var.payroll_email
|
|
||||||
SENDER_EMAIL = var.sender_email
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
depends_on = [
|
|
||||||
aws_cloudwatch_log_group.function,
|
|
||||||
aws_iam_role_policy.email_report,
|
|
||||||
aws_iam_role_policy_attachment.email_report_basic,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ locals {
|
||||||
"admin_authorizer",
|
"admin_authorizer",
|
||||||
"aggregate_orders",
|
"aggregate_orders",
|
||||||
"close_form",
|
"close_form",
|
||||||
"email_report",
|
|
||||||
"slack_notifier",
|
"slack_notifier",
|
||||||
"submit_order",
|
"submit_order",
|
||||||
"sync_roster",
|
"sync_roster",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ output "function_arns" {
|
||||||
admin_authorizer = aws_lambda_function.admin_authorizer.arn
|
admin_authorizer = aws_lambda_function.admin_authorizer.arn
|
||||||
aggregate_orders = aws_lambda_function.aggregate_orders.arn
|
aggregate_orders = aws_lambda_function.aggregate_orders.arn
|
||||||
close_form = aws_lambda_function.close_form.arn
|
close_form = aws_lambda_function.close_form.arn
|
||||||
email_report = aws_lambda_function.email_report.arn
|
|
||||||
slack_notifier = aws_lambda_function.slack_notifier.arn
|
slack_notifier = aws_lambda_function.slack_notifier.arn
|
||||||
submit_order = aws_lambda_function.submit_order.arn
|
submit_order = aws_lambda_function.submit_order.arn
|
||||||
sync_roster = aws_lambda_function.sync_roster.arn
|
sync_roster = aws_lambda_function.sync_roster.arn
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@ aws_region = "us-east-1"
|
||||||
domain_name = "orders.seahaven.com"
|
domain_name = "orders.seahaven.com"
|
||||||
attach_custom_domain = false
|
attach_custom_domain = false
|
||||||
|
|
||||||
# Payroll deduction report recipient and SES-verified sender.
|
|
||||||
payroll_email = "payroll@seahavenind.com"
|
|
||||||
sender_email = "adam@seahavenind.com"
|
|
||||||
|
|
||||||
# ARN of the out-of-band Secrets Manager secret holding the Slack bot token.
|
# ARN of the out-of-band Secrets Manager secret holding the Slack bot token.
|
||||||
# Only the ARN is used; the value never enters Terraform state.
|
# Only the ARN is used; the value never enters Terraform state.
|
||||||
slack_bot_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-XXXXXX"
|
slack_bot_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-XXXXXX"
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,6 @@ variable "attach_custom_domain" {
|
||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "payroll_email" {
|
|
||||||
description = "Recipient of the weekly payroll deduction report."
|
|
||||||
type = string
|
|
||||||
default = "payroll@seahavenind.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "sender_email" {
|
|
||||||
description = "SES-verified From address for the payroll deduction report."
|
|
||||||
type = string
|
|
||||||
default = "adam@seahavenind.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "slack_bot_secret_arn" {
|
variable "slack_bot_secret_arn" {
|
||||||
description = "ARN of the Secrets Manager secret holding the Slack bot token. The secret and its value are managed out-of-band; only the ARN enters this configuration."
|
description = "ARN of the Secrets Manager secret holding the Slack bot token. The secret and its value are managed out-of-band; only the ARN enters this configuration."
|
||||||
type = string
|
type = string
|
||||||
|
|
|
||||||
38
tests/test_terraform_email_report.py
Normal file
38
tests/test_terraform_email_report.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""email_report is removed from the live Terraform config (PLAT-135)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TERRAFORM = Path(__file__).resolve().parents[1] / "terraform"
|
||||||
|
|
||||||
|
|
||||||
|
def _read(name: str) -> str:
|
||||||
|
return (TERRAFORM / name).read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_email_report_not_in_function_packages():
|
||||||
|
locals_tf = _read("locals.tf")
|
||||||
|
assert '"email_report"' not in locals_tf
|
||||||
|
packages_sh = _read("build_packages.sh")
|
||||||
|
assert "email_report" not in packages_sh
|
||||||
|
|
||||||
|
|
||||||
|
def test_payroll_email_schedules_removed():
|
||||||
|
events = _read("events.tf")
|
||||||
|
assert "payroll-email-est" not in events
|
||||||
|
assert "payroll-email-edt" not in events
|
||||||
|
assert "email_report" not in events
|
||||||
|
|
||||||
|
|
||||||
|
def test_email_report_lambda_and_role_removed():
|
||||||
|
lambda_tf = _read("lambda.tf")
|
||||||
|
iam_tf = _read("iam.tf")
|
||||||
|
alarms = _read("alarms.tf")
|
||||||
|
outputs = _read("outputs.tf")
|
||||||
|
variables = _read("variables.tf")
|
||||||
|
assert "aws_lambda_function.email_report" not in lambda_tf
|
||||||
|
assert "aws_iam_role.email_report" not in iam_tf
|
||||||
|
assert "ses:SendRawEmail" not in iam_tf
|
||||||
|
assert "email-report" not in alarms
|
||||||
|
assert "email_report" not in outputs
|
||||||
|
assert "payroll_email" not in variables
|
||||||
|
assert "sender_email" not in variables
|
||||||
Loading…
Add table
Reference in a new issue