meal-order-manager/src/aggregator/aggregate.py

181 lines
5.9 KiB
Python
Raw Normal View History

"""
Order aggregation script.
Reads all submitted orders for a given week and produces:
1. Order summary — total quantity of each meal (for submitting to Redefine Meals)
2. Payroll deduction report — CSV with employee name, email, itemized meals, total amount
"""
import csv
import json
import sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
CONFIG_PATH = PROJECT_ROOT / "config.json"
ORDERS_DIR = PROJECT_ROOT / "orders"
OUTPUT_DIR = PROJECT_ROOT / "output"
def load_config():
with open(CONFIG_PATH) as f:
return json.load(f)
def current_week() -> str:
return datetime.now().strftime("%Y-W%U")
def load_orders(week: str) -> list[dict]:
week_dir = ORDERS_DIR / week
if not week_dir.exists():
return []
orders = []
for f in sorted(week_dir.glob("*.json")):
with open(f) as fh:
orders.append(json.load(fh))
return orders
def generate_order_summary(orders: list[dict]) -> dict:
"""Aggregate all meals across employees into a single order for Redefine."""
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
meal_totals = defaultdict(
lambda: {"quantity": 0, "bulk_price": 0, "employee_price": 0}
)
for order in orders:
for item in order.get("items", []):
name = item["name"]
meal_totals[name]["quantity"] += item.get("quantity", 0)
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
meal_totals[name]["bulk_price"] = item.get(
"bulk_price", item.get("price", 0)
)
meal_totals[name]["employee_price"] = item.get("price", 0)
summary = []
for name, data in sorted(meal_totals.items()):
summary.append(
{
"meal": name,
"quantity": data["quantity"],
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
"unit_price": data["bulk_price"],
"employee_unit_price": data["employee_price"],
"line_total": round(data["bulk_price"] * data["quantity"], 2),
"employee_line_total": round(
data["employee_price"] * data["quantity"], 2
),
}
)
grand_total = sum(s["line_total"] for s in summary)
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
employee_total = sum(s["employee_line_total"] for s in summary)
total_meals = sum(s["quantity"] for s in summary)
return {
"week": orders[0]["week"] if orders else "",
"generated_at": datetime.now().isoformat(),
"total_employees": len(orders),
"total_meals": total_meals,
"grand_total": round(grand_total, 2),
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
"employee_total": round(employee_total, 2),
"meals": summary,
}
def generate_payroll_csv(orders: list[dict], output_path: Path):
"""Write a CSV for payroll with one row per employee."""
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(
["Employee Name", "Employee Email", "Items Ordered", "Total Deduction"]
)
for order in sorted(orders, key=lambda o: o["employee_name"]):
items_str = "; ".join(
f"{item['name']} x{item['quantity']} (${item['subtotal']:.2f})"
for item in order.get("items", [])
)
writer.writerow(
[
order["employee_name"],
order["employee_email"],
items_str,
f"${order['total']:.2f}",
]
)
def generate_order_summary_csv(summary: dict, output_path: Path):
"""Write a CSV of the aggregated order for submitting to Redefine."""
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Meal", "Quantity", "Unit Price", "Line Total"])
for meal in summary["meals"]:
writer.writerow(
[
meal["meal"],
meal["quantity"],
f"${meal['unit_price']:.2f}",
f"${meal['line_total']:.2f}",
]
)
writer.writerow([])
writer.writerow(
["TOTAL", summary["total_meals"], "", f"${summary['grand_total']:.2f}"]
)
def main():
week = sys.argv[1] if len(sys.argv) > 1 else current_week()
orders = load_orders(week)
if not orders:
print(f"No orders found for week {week}")
print(f" Expected directory: {ORDERS_DIR / week}")
sys.exit(1)
print(f"Processing {len(orders)} orders for week {week}")
summary = generate_order_summary(orders)
OUTPUT_DIR.mkdir(exist_ok=True)
# Save order summary
summary_json = OUTPUT_DIR / f"order-summary-{week}.json"
with open(summary_json, "w") as f:
json.dump(summary, f, indent=2)
summary_csv = OUTPUT_DIR / f"order-summary-{week}.csv"
generate_order_summary_csv(summary, summary_csv)
# Save payroll report
payroll_csv = OUTPUT_DIR / f"payroll-deductions-{week}.csv"
generate_payroll_csv(orders, payroll_csv)
# Print summary
print(f"\n{'=' * 60}")
print(f"ORDER SUMMARY — Week {week}")
print(f"{'=' * 60}")
print(f"{'Meal':<45} {'Qty':>4} {'Total':>8}")
print("-" * 60)
for meal in summary["meals"]:
print(f"{meal['meal']:<45} {meal['quantity']:>4} ${meal['line_total']:>7.2f}")
print("-" * 60)
print(f"{'TOTAL':<45} {summary['total_meals']:>4} ${summary['grand_total']:>7.2f}")
Add discount pricing, Google auth, and order hardening (#10) * 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>
2026-05-13 18:00:21 -04:00
if (
summary.get("employee_total") is not None
and summary["employee_total"] != summary["grand_total"]
):
print(f"{'PAYROLL DEDUCTIONS':<45} ${summary['employee_total']:>7.2f}")
subsidy = round(summary["grand_total"] - summary["employee_total"], 2)
print(f"{'COMPANY SUBSIDY':<45} ${subsidy:>7.2f}")
print(f"\n{summary['total_employees']} employees ordered")
print("\nFiles generated:")
print(f" Order summary: {summary_csv}")
print(f" Payroll report: {payroll_csv}")
print(f" Raw JSON: {summary_json}")
if __name__ == "__main__":
main()