meal-order-manager/README.md
Adam Moussa 10d135e32e
Some checks are pending
Deploy / deploy (push) Waiting to run
Add weekly summary PDF and admin order-list download (#16) (#17)
Generate a per-person weekly summary PDF at Thursday close and store it
alongside the CSV reports, plus a client-side admin download that rolls
orders up into item -> total quantity for bulk ordering.

- shared/pdf.py: build_weekly_summary_pdf() via fpdf2 (pure-Python,
  ARM64-safe; first non-boto3 layer dep). Per-person employee -> item ->
  quantity, no pricing.
- aggregate_orders: write reports/{week}/weekly-summary-{week}.pdf
  (application/pdf) and stamp weekly_summary_pdf_s3_key on the SUMMARY.
  No new IAM (existing S3CrudPolicy). No email/Slack delivery.
- generate_form.py: "Download order list" admin button aggregates the
  loaded week's orders into an item->qty CSV (no per-employee breakdown,
  no prices) via a Blob download. Works for open weeks too.
- Tests: tests/test_pdf.py; aggregate happy-path now asserts 3 S3
  uploads + the pdf key.
- README updated.
2026-06-01 18:49:58 -04:00

160 lines
8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# meal-order-manager
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 │ + 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**); stored only, not emailed |
## 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.
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 |
## 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. 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:
```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)
- `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
```