Automated weekly meal ordering from Redefine Meals — scraper, order form, payroll deductions
Find a file
Adam Moussa adf175daef
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(form): render admin orders as mobile cards (#86)
* feat(form): render admin orders as mobile cards

* fix(form): address mobile admin review findings

* fix(tests): remove unused mobile fixture state
2026-07-31 10:15:43 -04:00
.github refactor(form): extract Jinja templates and lock form JS in CI (#77) 2026-07-30 19:19:51 -04:00
.security-review chore(security): add repo-local suppression for test-fixture FP (gitleaks-45) (#48) 2026-07-13 14:30:43 -04:00
functions chore(deps): bump boto3 from 1.43.51 to 1.43.56 in multiple files (#68) 2026-07-27 15:03:46 +00:00
scripts Add admin panel, fix dual-domain auth, harden scrape schedule (#14) 2026-05-19 16:32:19 -04:00
src feat(form): render admin orders as mobile cards (#86) 2026-07-31 10:15:43 -04:00
tests feat(form): render admin orders as mobile cards (#86) 2026-07-31 10:15:43 -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 docs: link Confluence AWS Architecture Map (INFRA-53) (#35) 2026-07-06 17:43:54 -04:00
requirements.txt refactor(form): extract Jinja templates and lock form JS in CI (#77) 2026-07-30 19:19:51 -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 Add CloudWatch alarm coverage (meal-order-manager) (#32) 2026-06-17 14:45:54 -04:00

meal-order-manager

Python AWS SAM Slack CI

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 — 7 functions: submit-order, admin-authorizer, 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
  • CloudWatch Alarms — coverage across the stack, all notifying the shared site-alerts SNS topic (see Monitoring)

Monitoring

CloudWatch alarms are defined in template.yaml. Every alarm sends to the shared site-alerts SNS topic (arn:aws:sns:us-east-1:328440206208:site-alerts), has no 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>.

  • Lambda Errors / Throttles — one alarm each per function (7 functions), Sum over 5 min, fires on any error/throttle (threshold 0).
  • Lambda Duration — p99 over 5 min at ~80% of each function's timeout. API-fronted functions (submit-order, admin-authorizer) evaluate 3/3 datapoints; cron/async functions evaluate a single datapoint.
  • DynamoDB orders table — ReadThrottleEvents and WriteThrottleEvents (TableName dimension). DynamoDB does not publish ThrottledRequests/SystemErrors at the table-only dimension, so those are intentionally not alarmed.
  • API Gateway (OrderApi, HTTP API v2) — 5xx (threshold 0), 4xx (threshold 20, 3/2 datapoints to absorb routine 401s from the token authorizer), and p99 Latency (~3000ms).

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)

Documentation

The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's meal-order-manager stack is represented there as a Mermaid subgraph.

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