* fix(test): mock get_settings/get_roster in aggregated tests + enable CI tests (INFRA-72)
handle_orders_aggregated now delivers the summary via admin DMs (PR #15),
adding get_settings/get_roster calls the aggregated tests never mocked, so
they hit live DynamoDB. Mock both and assert the message content on the
send_dm path. Set run-tests: true so the suite actually runs in CI.
* fix(test): default AWS region in conftest so CI collection doesn't hit NoRegionError (INFRA-72)
Handlers build boto3 clients at module load; CI runners have no AWS config,
so test collection raised NoRegionError once the suite actually ran. Set a
region default before imports (offline client construction; calls are mocked).
* fix(test): add repo root to sys.path so CI's bare pytest collects functions.* (INFRA-72)
test_aggregate_orders imports functions.aggregate_orders.handler, which needs
the repo root on sys.path. python -m pytest injects CWD automatically but CI
runs pytest directly, so these 11 tests errored at collection in CI only.
* Add CloudWatch alarm coverage for the meal-order-manager stack
Add CloudWatch alarms (all notifying the shared site-alerts SNS topic,
no OKActions, TreatMissingData notBreaching) across the stack:
- Lambda Errors + Throttles alarms for all 7 functions (Sum, 5min,
threshold 0).
- Lambda Duration p99 alarms at ~80% of each function's timeout;
API-fronted functions eval 3/3, cron/async functions eval 1/1.
Thresholds pending sign-off.
- DynamoDB orders-table Read/WriteThrottleEvents alarms (TableName dim).
ThrottledRequests/SystemErrors are not published at the table-only
dimension, so they are intentionally omitted.
- API Gateway (OrderApi v2) 5xx, 4xx (threshold 20, 3/2 to absorb
routine authorizer 401s), and p99 Latency alarms.
Update README with a Monitoring section and correct the Lambda count
to 7 (admin-authorizer was missing).
* Drop pending-sign-off wording from alarm docs
Duration/Latency thresholds are owner-approved; remove PENDING ADAM
SIGN-OFF / pending-sign-off notes from template.yaml comments and README.
aggregate_orders uploads the weekly PDF/CSVs to S3 and then calls
put_summary(), but put_summary spread the summary dict (which contains
float prices/totals) straight into put_item without Decimal conversion.
boto3 rejects floats (TypeError: Float types are not supported), so the
SUMMARY DynamoDB item was never written for any week (W20-W23).
The summary-PDF download endpoint gates on get_summary(week), so it got
None and returned 404 -- 'No summary PDF for <week> yet' -- even though
the PDF was sitting in S3.
Convert via _to_decimal in put_summary, matching put_order/put_settings.
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
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
Address GPT-4.1 cross-review of the IAM change:
- Validate the week param (YYYY-WNN) and the DynamoDB-sourced PDF key
shape before presigning, so a tampered SUMMARY record can't mint URLs
for other report files (payroll CSVs).
- Narrow the IAM resource from reports/* to
reports/*/weekly-summary-*.pdf — least privilege over the bucket.
- Reuse a module-level S3 client; name the URL TTL constant.
- Distinguish "week not found" from "PDF key missing" 404s.
- Tests: malformed-week 400 and tampered-key 500 (asserts no presign).
The weekly summary PDF generated at Thursday close was stored in the
reports bucket with no way to reach it from the UI — admins had to pull
it from S3 manually. Surface it in the admin panel:
- submit_order: GET /api/admin/summary-pdf?week= (admin-gated) returns
a 5-minute presigned URL from the SUMMARY item's stamped PDF key;
404 for weeks that haven't closed.
- template.yaml: REPORTS_BUCKET env var + read-only s3:GetObject on
reports/* for SubmitOrderFunction (needed so the presigned URL is
signed with sufficient permissions).
- generate_form.py: "Download summary PDF" button in the admin header;
explains Thursday-close timing on 404.
- Tests: presign happy path, 404 open week, 400 missing week, 401
unauthenticated.
- README updated.
Bump aws-actions/configure-aws-credentials to @v6 (org target) in the
weekly-menu workflow. v6 is the verified org standard alongside
actions/checkout@v6.
Ref: engineering-handbook cicd.md (workflow standardization).
* Add dependency-review caller workflow
Add a pull_request-triggered caller that invokes the org-level
callable-dependency-review workflow to scan dependency changes and
fail on high-severity advisories.
* chore: retrigger checks
* chore: retrigger dep review (post-fix)
Re-adds WebACLId (from SSM /seahaven/waf/app-web-acl-arn) now that the
github-cfn-execution-role has wafv2 perms. Deployed + verified: orders.seahaven.com
distribution now fronted by seahaven-app-waf.
* Add WAF + API access logging/throttling (audit Day 3: M-17, M-18)
- M-17: associate the shared seahaven-app-waf CloudFront WebACL (ARN from SSM
/seahaven/waf/app-web-acl-arn) with the orders.seahaven.com distribution.
- M-18: enable HTTP API access logging to /aws/apigateway/meal-order-manager
(90d) + default route throttling (100 rps, 50 burst) on OrderApi.
* Defer M-17 WAF association (deploy role lacks wafv2)
The github-cfn-execution-role (sticky CFN service role on this stack) has
cloudfront:* + ssm:* but no wafv2:*, so associating the WebACL fails with
'Unable to verify read permissions on Web ACL'. Landing M-18 (access logging +
throttling) now; WAF association re-added once the deploy role gets wafv2 perms
(tracked separately).
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.
The closed overlay (z-index 2000) was blocking Google sign-in from
firing, so the admin check never ran. Now the overlay is deferred
when Google auth is configured — checkAdmin() shows or bypasses
it after auth completes.
Admins (by email in settings) can now view and submit orders even
after the form closes. The orders-aggregated Slack summary is sent
as a DM to each admin instead of posting to the channel.
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
- Change custom domain from orders.seahavenind.com to
orders.seahaven.com to match other subdomain conventions
- Add GET /api/roster endpoint returning employee names/emails
- Replace name/email text inputs with dropdown populated from
roster API (falls back to embedded roster for local dev)
- Fix order deadline text from Wednesday to Thursday 11:59 PM
- Fix Slack API calls: use form-urlencoded for conversations and
users methods that reject JSON body encoding
conversations.members, conversations.open, and users.info reject
JSON body with 'missing required field'. Use form-urlencoded for
these methods while keeping JSON for chat.postMessage and
files.upload which require it for structured blocks/payloads.
Use cfn-role-arn as input (not secret), pass deploy-role-arn and
parameter-overrides as secrets, add permissions and concurrency
blocks matching the standard org pattern.
Apply ruff check --fix and ruff format across all Python files to
pass CI pipeline. Remove unused imports (os, sys), fix f-strings
without placeholders. Update README to reflect sync-roster Lambda,
corrected shared layer path, and current project structure.
- Add sync-roster Lambda that auto-syncs employee roster from Slack
channel membership (runs Monday 6:55am ET before menu publish)
- Move FormApiKey from CloudFormation parameter/env var to Secrets
Manager (meal-order-manager/form-api-key) per security conventions
- Add Slack app manifest with required bot scopes
- Add get_channel_members() and get_user_info() to shared Slack module
- Add Lambda function ARN outputs to CloudFormation
- Add log group for sync-roster Lambda (60-day retention)
Move shared layer source from src/shared/python/shared/ to
src/shared/shared/ to prevent SAM from creating a double
python/python/ directory in the layer artifact. Add _to_decimal()
helper to convert floats to Decimal for DynamoDB compatibility
in put_order.
Playwright-based menu scraper for Redefine Meals, self-contained HTML
order form with S3/CloudFront hosting, DynamoDB-backed order submission
via API Gateway, and automated payroll deduction reports via SES.