meal-order-manager/README.md

221 lines
13 KiB
Markdown
Raw Normal View History

2026-05-12 18:24:10 -04:00
# meal-order-manager
![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white)
![AWS SAM](https://img.shields.io/badge/AWS-SAM-FF9900?logo=amazonaws&logoColor=white)
![Slack](https://img.shields.io/badge/Slack-integration-4A154B?logo=slack&logoColor=white)
![CI](https://github.com/Sea-Haven-Industries/meal-order-manager/actions/workflows/ci.yml/badge.svg)
Automates weekly meal ordering from [Redefine Meals](https://www.redefinemeals.com) 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 │ + signed API │ (CloudFront+S3) │──POST───┐ │ - Aggregate │
│ - Slack notify │ └──────────────────┘ │ │ - Slack summary │
└─────────────────┘ ▼ └──────────────────┘
┌──────────┐
Thu 10am: Slack DM │ API GW + │
reminders to employees │ Lambda │
who haven't ordered │ submit │
└────┬─────┘
▼
┌──────────┐
│ DynamoDB │
│ orders │
└──────────┘
```
### Form frontend decisions
- **Theme:** Retain the current order-form palette and typography. There is no shared Sea Haven web design system to adopt, and changing the theme without one would be an isolated visual redesign. Future theme changes should remap the existing CSS custom properties instead of adding scattered color values or inline styles.
- **Deployment:** Keep generating and publishing one self-contained HTML file. Inlining the template CSS and JavaScript at build time preserves the current atomic S3 upload and avoids introducing asset versioning, cache coordination, and additional CloudFront invalidation paths. Revisit a multi-file deploy only when asset size, cross-page sharing, or independent caching provides a concrete benefit.
- **Visual regression:** Do not add visual-regression CI now. Structural and Playwright coverage remain the active safeguards. Revisit snapshots, computed-style assertions, or a hosted service only after a layout or token regression reaches production without those checks catching it.
ESLint and Prettier check the JavaScript template sources in CI. They do not lint
the generated HTML, and the generated deployment artifact remains self-contained.
## Weekly Flow
| When | What | How |
|------|------|-----|
| Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB |
| 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 |
### Weekly menu publication boundary
The scheduled GitHub workflow has no DynamoDB permissions. It signs two requests
with its short-lived OIDC role credentials:
- `GET /api/publish/settings` returns only the bulk discount and company subsidy.
- `POST /api/publish/menu` validates and writes the current Eastern-time week's menu.
Both routes use API Gateway `AWS_IAM` authorization and invoke the existing
submit-order Lambda. The GitHub role can invoke only these method and path
combinations. Employee orders, the roster, admin configuration, and all direct
DynamoDB actions remain inaccessible to the role.
Deploy the API routes before switching the workflow and IAM policy. No data
migration is required because the Lambda writes the existing `WEEK#...` / `MENU`
record shape. To roll back, restore the previous workflow and its DynamoDB policy
together; restoring only the policy does not make the API-based workflow depend on
DynamoDB access.
### 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, admin-authorizer, close-form, aggregate-orders, slack-notifier, sync-roster
- **EventBridge** — scheduled rules (dual EST/EDT) for close, reminders, roster sync
- **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.
- **CloudWatch Alarms** — coverage across the stack, all notifying the shared `site-alerts` SNS topic (see Monitoring)
- **HCP Terraform** — workspace `meal-order-manager-prod` in project `seahaven-prod` is the sole apply path. Working directory `terraform/`. VCS file triggers use `trigger-patterns = [terraform/**/*, src/**/*, functions/**/*]` because `terraform/build_packages.sh` packages the shared layer from `src/shared` and the handlers from `functions/`. A `src/`-only or `functions/`-only merge must still queue a run. Do not `terraform apply` locally to prod.
## 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 (6 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). The submit route is limited to 5 requests/second with a burst of 10.
## Authentication
Google Identity Services (OAuth) with tokeninfo endpoint verification. Accepts both `seahavenind.com` and `seahaven.com` Google Workspace domains. Cloud form generation and Lambda order submission fail closed unless `/meal-order-manager/google-client-id` is configured. The local Flask workflow can still use manual name and email entry when Google auth is not configured.
## 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.
- **[AWS Architecture Map](https://seahaven.atlassian.net/wiki/spaces/IT/pages/1540098)** (Confluence, IT space, page 1540098)
## Setup
### Local development
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
```
### Deploy to AWS
```bash
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. 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`
4. 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`
## Local Workflow (no AWS)
The scraper, form generator, Flask server, and aggregator still work locally:
```bash
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)
- `google_client_id` — optional locally; required for cloud generation
- `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
├── terraform/ # HCP Terraform (workspace meal-order-manager-prod)
├── 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/
├── 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
```