Automated weekly meal ordering from Redefine Meals — scraper, order form, payroll deductions
Find a file
Adam Moussa 921efe2c04
Some checks are pending
Deploy / deploy (push) Waiting to run
Attach permissions boundary to all IAM roles (#25)
Scope-down requirement from INFRA-97: github-cfn-execution-role
needs iam:CreateRole scoped to roles that carry the org boundary,
so every role this stack creates must declare it.

- Globals.Function.PermissionsBoundary: applies to all six
  SAM auto-generated Lambda execution roles
- AdminAuthorizerInvokeRole: adds PermissionsBoundary + Path
  /cfn-managed/ (explicit AWS::IAM::Role)

The only consumer of AdminAuthorizerInvokeRole is the HttpApi
authorizer's FunctionInvokeRole, which references it via
!GetAtt AdminAuthorizerInvokeRole.Arn — no hardcoded ARN
strings, so the path change is safe.

Refs: INFRA-103
2026-06-10 14:14:54 -04:00
.github/workflows chore(ci): bump configure-aws-credentials to v6 (#20) 2026-06-05 12:39:35 -04:00
functions Add gateway-level authorizer to admin API (INFRA-100) (#24) 2026-06-08 18:05:09 -04:00
scripts Add admin panel, fix dual-domain auth, harden scrape schedule (#14) 2026-05-19 16:32:19 -04:00
src Add admin summary-PDF download endpoint and button 2026-06-05 18:41:55 -04:00
tests Add gateway-level authorizer to admin API (INFRA-100) (#24) 2026-06-08 18:05:09 -04:00
.gitignore Initial commit: meal ordering automation system (#1) 2026-05-12 18:25:15 -04:00
config.json Add discount pricing, Google auth, and order hardening (#10) 2026-05-13 18:00:21 -04:00
README.md Add admin summary-PDF download endpoint and button 2026-06-05 18:41:55 -04:00
requirements.txt Initial commit: meal ordering automation system (#1) 2026-05-12 18:25:15 -04:00
samconfig.toml.example Switch to orders.seahaven.com, add roster dropdown, fix deadline (#9) 2026-05-12 20:16:46 -04:00
slack-app-manifest.yml Fix Slack manifest long_description to meet 174-char minimum (#4) 2026-05-12 19:22:46 -04:00
template.yaml Attach permissions boundary to all IAM roles (#25) 2026-06-10 14:14:54 -04:00

meal-order-manager

Automates weekly meal ordering from Redefine Meals for Sea Haven Industries employees. Scrapes the menu, generates an order form, collects individual orders, and produces payroll deduction reports plus a per-person weekly summary PDF.

Architecture

Monday 7:30am ET                     Employees (Mon–Thu)               Thursday 6pm ET
┌─────────────────┐                  ┌──────────────────┐              ┌──────────────────┐
│  GitHub Actions  │                 │  orders.seahaven │              │  EventBridge      │
│  - Scrape menu   │────S3 upload───▶│  ind.com         │              │  - Close form     │
│  - Generate form │  + DynamoDB     │  (CloudFront+S3) │──POST───┐    │  - Aggregate      │
│  - Slack notify  │                 └──────────────────┘         │    │  - Slack summary  │
└─────────────────┘                                               ▼    └──────────────────┘
                                                          ┌──────────┐
Monday 7am ET                                             │ API GW + │
┌──────────────────┐                                      │ Lambda   │
│  EventBridge     │                                      │ submit   │
│  - Email payroll │                                      └────┬─────┘
│    deductions    │                                           ▼
└──────────────────┘                                      ┌──────────┐
                                                          │ DynamoDB │
Thu 10am: Slack DM                                        │ orders   │
reminders to employees                                    └──────────┘
who haven't ordered

Weekly Flow

When What How
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
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 6pm ET Close form, aggregate orders, write CSV reports + weekly summary PDF, post Redefine order summary to Slack EventBridge → Lambda chain

Reports (written to meal-order-manager-reports-* at Thursday close)

Key Contents
reports/{week}/order-summary.csv Meal-level aggregate (meal, qty, unit price, line total) for the Redefine order
reports/{week}/payroll-deductions.csv Per-employee payroll deduction totals
reports/{week}/weekly-summary-{week}.pdf Per-person summary (employee → item → quantity, no pricing); downloadable from the admin panel via presigned URL

AWS Resources

Stack name: meal-order-manager (us-east-1)

  • S3 — meal-order-manager-form-* (static form hosting), meal-order-manager-reports-* (CSV reports + weekly summary PDF)
  • CloudFront — HTTPS distribution with custom domain orders.seahaven.com
  • DynamoDB — meal-order-manager-orders (orders, menu, roster, config)
  • API Gateway — HttpApi for order submission and admin operations
  • Lambda — 6 functions: submit-order, close-form, aggregate-orders, slack-notifier, sync-roster, email-report
  • EventBridge — scheduled rules (dual EST/EDT) for close, reminders, payroll email
  • Secrets Manager — Slack bot token, form API key
  • SES — payroll deduction emails

Authentication

Google Identity Services (OAuth) with tokeninfo endpoint verification. Accepts both seahavenind.com and seahaven.com Google Workspace domains.

Admin Panel

Admins (configured in DynamoDB CONFIG/SETTINGS → admin_emails list) get an "Admin" button after Google sign-in. The panel provides:

  • View all orders by week with totals
  • Edit order quantities, add new menu items, remove items
  • Delete orders entirely
  • Download order list — a CSV rollup of item → total quantity across all employees (no per-employee breakdown, no prices) to drive the bulk Redefine order. Generated client-side from the loaded week, so it works for open weeks too.
  • Download summary PDF — fetches a short-lived presigned URL for the week's per-person summary PDF (generated at Thursday close) and opens it. Returns 404 for weeks that haven't closed yet.

All admin operations enforce server-side price recalculation from the menu.

API routes (all require Google auth + admin email):

Method Path Description
GET /api/admin/orders List weeks with order counts
GET /api/admin/orders?week=YYYY-WNN Get all orders for a week
PUT /api/admin/orders Update an order (recalculates prices)
DELETE /api/admin/orders?week=...&email=... Delete an order
GET /api/admin/summary-pdf?week=YYYY-WNN Presigned URL for the week's summary PDF (404 if week not closed)

Setup

Local development

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium

Deploy to AWS

cp samconfig.toml.example samconfig.toml
# Edit samconfig.toml with your certificate ARN, etc.
sam build
sam deploy

Post-deploy

  1. Create the Slack bot token secret: aws secretsmanager create-secret --name meal-order-manager/slack-bot-token --secret-string "xoxb-..."
  2. Create the form API key secret: aws secretsmanager create-secret --name meal-order-manager/form-api-key --secret-string "$(openssl rand -hex 32)"
  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
  5. Set up DNS: CNAME orders.seahaven.com → CloudFront distribution domain
  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)

The scraper, form generator, Flask server, and aggregator still work locally:

python3 src/scraper/scrape_menu.py        # scrape menu
python3 src/server/generate_form.py       # generate form (local mode)
python3 src/server/app.py                 # serve on localhost:5050
python3 src/aggregator/aggregate.py       # generate CSV reports

Configuration

config.json (local dev):

  • menu_url — Redefine Meals menu URL
  • order_deadline — displayed on the form
  • roster — employee list (name, email, slack_user_id)
  • output_dir / orders_dir — local output paths

Project Structure

meal-order-manager/
├── .github/workflows/
│   ├── weekly-menu.yml          # Monday cron: scrape + publish + notify
│   ├── ci.yml                   # PR checks
│   └── deploy.yml               # Push to main: sam deploy
├── src/
│   ├── scraper/                 # Playwright menu scraper
│   ├── server/                  # Form generator + local Flask server
│   ├── aggregator/              # Order aggregation + CSV reports
│   └── shared/shared/           # Lambda layer (db, secrets, slack, pdf helpers)
├── functions/                   # Lambda handlers
│   ├── submit_order/
│   ├── close_form/
│   ├── aggregate_orders/
│   ├── slack_notifier/
│   ├── sync_roster/
│   └── email_report/
├── scripts/                     # CI/CD helper scripts
│   ├── upload_menu.py
│   ├── notify_slack.py
│   └── seed_roster.py
├── template.yaml                # SAM template
├── samconfig.toml.example
├── slack-app-manifest.yml       # Slack app manifest (paste into api.slack.com)
└── config.json