2026-05-12 18:25:15 -04:00
|
|
|
"""
|
|
|
|
|
Lightweight Flask server for the meal order form.
|
|
|
|
|
|
|
|
|
|
Serves the generated HTML form and handles order submissions.
|
|
|
|
|
Orders are saved as JSON files in the orders directory, one per employee per week.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
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
|
|
|
import time
|
|
|
|
|
import urllib.request
|
2026-05-12 18:25:15 -04:00
|
|
|
from datetime import datetime
|
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
|
|
|
from decimal import Decimal, ROUND_HALF_UP
|
2026-05-12 18:25:15 -04:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
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
|
|
|
import boto3
|
2026-05-12 18:25:15 -04:00
|
|
|
from flask import Flask, jsonify, request, send_file
|
|
|
|
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
CONFIG_PATH = PROJECT_ROOT / "config.json"
|
|
|
|
|
OUTPUT_DIR = PROJECT_ROOT / "output"
|
|
|
|
|
ORDERS_DIR = PROJECT_ROOT / "orders"
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 latest_menu_file() -> Path | None:
|
|
|
|
|
files = sorted(OUTPUT_DIR.glob("menu-*.json"), reverse=True)
|
|
|
|
|
return files[0] if files else None
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
def _official_menu_retail_by_name() -> dict[str, Decimal]:
|
|
|
|
|
menu_file = latest_menu_file()
|
|
|
|
|
if not menu_file:
|
|
|
|
|
return {}
|
|
|
|
|
with open(menu_file) as f:
|
|
|
|
|
meals = (json.load(f) or {}).get("meals") or []
|
|
|
|
|
out: dict[str, Decimal] = {}
|
|
|
|
|
for meal in meals:
|
|
|
|
|
name = (meal.get("name") or "").strip()
|
|
|
|
|
if not name or meal.get("price") is None:
|
|
|
|
|
continue
|
|
|
|
|
out[name] = Decimal(str(meal["price"]))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
@app.route("/")
|
|
|
|
|
def index():
|
|
|
|
|
form_file = OUTPUT_DIR / f"order-form-{current_week()}.html"
|
|
|
|
|
if not form_file.exists():
|
|
|
|
|
return "No order form generated for this week. Run generate_form.py first.", 404
|
|
|
|
|
return send_file(form_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/menu")
|
|
|
|
|
def get_menu():
|
|
|
|
|
menu_file = latest_menu_file()
|
|
|
|
|
if not menu_file:
|
2026-05-12 19:31:27 -04:00
|
|
|
return jsonify(
|
|
|
|
|
{"error": "No menu data available. Run scrape_menu.py first."}
|
|
|
|
|
), 404
|
2026-05-12 18:25:15 -04:00
|
|
|
with open(menu_file) as f:
|
|
|
|
|
return jsonify(json.load(f))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/roster")
|
|
|
|
|
def get_roster():
|
|
|
|
|
config = load_config()
|
|
|
|
|
return jsonify(config.get("roster", []))
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
# Match functions/submit_order/handler.py: TTL so a transient SSM failure cannot
|
|
|
|
|
# pin client_id to "" for the process lifetime (which would skip Google auth).
|
|
|
|
|
_GOOGLE_CLIENT_ID_CACHE_TTL_SECONDS = 300
|
|
|
|
|
|
|
|
|
|
_google_client_id_cache: str | None = None
|
|
|
|
|
_google_client_id_cache_ts = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_google_client_id() -> str:
|
|
|
|
|
global _google_client_id_cache, _google_client_id_cache_ts
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if (
|
|
|
|
|
_google_client_id_cache is not None
|
|
|
|
|
and (now - _google_client_id_cache_ts) <= _GOOGLE_CLIENT_ID_CACHE_TTL_SECONDS
|
|
|
|
|
):
|
|
|
|
|
return _google_client_id_cache
|
|
|
|
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
from_config = (config.get("google_client_id") or "").strip()
|
|
|
|
|
if from_config:
|
|
|
|
|
_google_client_id_cache = from_config
|
|
|
|
|
_google_client_id_cache_ts = now
|
|
|
|
|
return _google_client_id_cache
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
ssm = boto3.client("ssm")
|
|
|
|
|
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
|
|
|
|
|
_google_client_id_cache = (resp["Parameter"].get("Value") or "").strip()
|
|
|
|
|
except Exception:
|
|
|
|
|
_google_client_id_cache = ""
|
|
|
|
|
_google_client_id_cache_ts = now
|
|
|
|
|
return _google_client_id_cache
|
|
|
|
|
|
|
|
|
|
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* 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.
2026-05-19 16:32:19 -04:00
|
|
|
ALLOWED_DOMAINS = {"seahavenind.com", "seahaven.com"}
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
def _verify_google_token(token: str, client_id: str) -> dict | None:
|
|
|
|
|
if not client_id:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
f"https://oauth2.googleapis.com/tokeninfo?id_token={token}"
|
|
|
|
|
)
|
|
|
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
|
|
|
data = json.loads(resp.read())
|
|
|
|
|
if data.get("aud") != client_id:
|
|
|
|
|
return None
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* 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.
2026-05-19 16:32:19 -04:00
|
|
|
if data.get("hd") not in ALLOWED_DOMAINS:
|
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
|
|
|
return None
|
|
|
|
|
return {"name": data.get("name", ""), "email": data.get("email", "")}
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
@app.route("/api/submit-order", methods=["POST"])
|
|
|
|
|
def submit_order():
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({"error": "No data received"}), 400
|
|
|
|
|
|
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
|
|
|
client_id = _get_google_client_id()
|
|
|
|
|
google_token = data.get("google_id_token")
|
|
|
|
|
|
|
|
|
|
if client_id:
|
|
|
|
|
if not google_token:
|
|
|
|
|
return jsonify({"error": "Google authentication is required"}), 403
|
|
|
|
|
user_info = _verify_google_token(google_token, client_id)
|
|
|
|
|
if not user_info:
|
|
|
|
|
return jsonify({"error": "Invalid or unauthorized Google account"}), 403
|
|
|
|
|
name = user_info["name"]
|
|
|
|
|
email = user_info["email"]
|
|
|
|
|
else:
|
|
|
|
|
name = data.get("employee_name", "").strip()
|
|
|
|
|
email = data.get("employee_email", "").strip()
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
items = data.get("items", [])
|
|
|
|
|
|
|
|
|
|
if not name:
|
|
|
|
|
return jsonify({"error": "Employee name is required"}), 400
|
|
|
|
|
if not email:
|
|
|
|
|
return jsonify({"error": "Employee email is required"}), 400
|
|
|
|
|
if not items or not any(i.get("quantity", 0) > 0 for i in items):
|
|
|
|
|
return jsonify({"error": "Please select at least one meal"}), 400
|
|
|
|
|
|
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
|
|
|
config = load_config()
|
|
|
|
|
TWO_PLACES = Decimal("0.01")
|
|
|
|
|
bulk_pct = Decimal(str(config.get("bulk_discount_percent", 0)))
|
|
|
|
|
subsidy_pct = Decimal(str(config.get("company_subsidy_percent", 0)))
|
|
|
|
|
bulk_pct = max(Decimal("0"), min(Decimal("100"), bulk_pct))
|
|
|
|
|
subsidy_pct = max(Decimal("0"), min(Decimal("100"), subsidy_pct))
|
|
|
|
|
bulk_mult = Decimal("1") - (bulk_pct / Decimal("100"))
|
|
|
|
|
subsidy_mult = Decimal("1") - (subsidy_pct / Decimal("100"))
|
|
|
|
|
|
|
|
|
|
filtered = [i for i in items if i.get("quantity", 0) > 0]
|
|
|
|
|
official_retail = _official_menu_retail_by_name()
|
|
|
|
|
if not official_retail:
|
|
|
|
|
return jsonify({"error": "Menu temporarily unavailable"}), 503
|
|
|
|
|
for item in filtered:
|
|
|
|
|
meal_name = (item.get("name") or "").strip()
|
|
|
|
|
if meal_name not in official_retail:
|
|
|
|
|
return jsonify(
|
|
|
|
|
{"error": "One or more meals are not on this week's menu"}
|
|
|
|
|
), 400
|
|
|
|
|
|
|
|
|
|
for item in filtered:
|
|
|
|
|
meal_name = (item.get("name") or "").strip()
|
|
|
|
|
retail = official_retail[meal_name]
|
|
|
|
|
qty = Decimal(str(item.get("quantity", 0)))
|
|
|
|
|
bulk_price = (retail * bulk_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
|
|
|
|
emp_price = (bulk_price * subsidy_mult).quantize(
|
|
|
|
|
TWO_PLACES, rounding=ROUND_HALF_UP
|
|
|
|
|
)
|
|
|
|
|
subtotal = (emp_price * qty).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
|
|
|
|
item["retail_price"] = float(retail)
|
|
|
|
|
item["bulk_price"] = float(bulk_price)
|
|
|
|
|
item["price"] = float(emp_price)
|
|
|
|
|
item["subtotal"] = float(subtotal)
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
week = current_week()
|
|
|
|
|
week_dir = ORDERS_DIR / week
|
|
|
|
|
week_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
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
|
|
|
# Match Lambda: one order file per employee email (not display name).
|
|
|
|
|
slug = email.strip().lower()
|
|
|
|
|
slug_safe = slug.replace("/", "_").replace("\\", "_")
|
|
|
|
|
order_file = week_dir / f"{slug_safe}.json"
|
|
|
|
|
|
|
|
|
|
total = float(
|
|
|
|
|
sum(Decimal(str(i["subtotal"])) for i in filtered).quantize(
|
|
|
|
|
TWO_PLACES, rounding=ROUND_HALF_UP
|
|
|
|
|
)
|
2026-05-12 19:31:27 -04:00
|
|
|
)
|
2026-05-12 18:25:15 -04:00
|
|
|
|
|
|
|
|
order = {
|
|
|
|
|
"employee_name": name,
|
|
|
|
|
"employee_email": email,
|
|
|
|
|
"week": week,
|
|
|
|
|
"submitted_at": datetime.now().isoformat(),
|
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
|
|
|
"items": filtered,
|
|
|
|
|
"total": total,
|
2026-05-12 18:25:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with open(order_file, "w") as f:
|
|
|
|
|
json.dump(order, f, indent=2)
|
|
|
|
|
|
2026-05-12 19:31:27 -04:00
|
|
|
return jsonify(
|
|
|
|
|
{"status": "ok", "message": f"Order saved for {name}", "total": order["total"]}
|
|
|
|
|
)
|
2026-05-12 18:25:15 -04:00
|
|
|
|
|
|
|
|
|
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
|
|
|
@app.route("/api/form-status/<week>")
|
|
|
|
|
def form_status(week: str):
|
|
|
|
|
return jsonify({"week": week, "status": "open"})
|
|
|
|
|
|
|
|
|
|
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* 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.
2026-05-19 16:32:19 -04:00
|
|
|
@app.route("/api/admin/orders")
|
|
|
|
|
def admin_orders():
|
|
|
|
|
week = request.args.get("week")
|
|
|
|
|
if not week:
|
|
|
|
|
weeks = []
|
|
|
|
|
for f in sorted(ORDERS_DIR.iterdir(), reverse=True):
|
|
|
|
|
if f.is_dir():
|
|
|
|
|
order_count = len(list(f.glob("*.json")))
|
|
|
|
|
weeks.append(
|
|
|
|
|
{
|
|
|
|
|
"week": f.name,
|
|
|
|
|
"form_status": "open",
|
|
|
|
|
"meal_count": 0,
|
|
|
|
|
"order_count": order_count,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return jsonify({"weeks": weeks})
|
|
|
|
|
|
|
|
|
|
week_dir = ORDERS_DIR / week
|
|
|
|
|
if not week_dir.exists():
|
|
|
|
|
return jsonify(
|
|
|
|
|
{"week": week, "orders": [], "total_employees": 0, "grand_total": 0}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
orders = []
|
|
|
|
|
for f in sorted(week_dir.glob("*.json")):
|
|
|
|
|
with open(f) as fh:
|
|
|
|
|
orders.append(json.load(fh))
|
|
|
|
|
orders.sort(key=lambda o: o.get("employee_name", ""))
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"week": week,
|
|
|
|
|
"orders": orders,
|
|
|
|
|
"total_employees": len(orders),
|
|
|
|
|
"grand_total": round(sum(o.get("total", 0) for o in orders), 2),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/admin/orders", methods=["DELETE"])
|
|
|
|
|
def admin_delete_order():
|
|
|
|
|
week = request.args.get("week", "")
|
|
|
|
|
email = request.args.get("email", "")
|
|
|
|
|
if not week or not email:
|
|
|
|
|
return jsonify({"error": "week and email are required"}), 400
|
|
|
|
|
|
|
|
|
|
slug = email.strip().lower().replace("/", "_").replace("\\", "_")
|
|
|
|
|
order_file = ORDERS_DIR / week / f"{slug}.json"
|
|
|
|
|
if not order_file.exists():
|
|
|
|
|
return jsonify({"error": "Order not found"}), 404
|
|
|
|
|
|
|
|
|
|
order_file.unlink()
|
|
|
|
|
return jsonify({"status": "deleted", "week": week, "email": email})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/admin/orders", methods=["PUT"])
|
|
|
|
|
def admin_update_order():
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({"error": "No data received"}), 400
|
|
|
|
|
|
|
|
|
|
week = data.get("week", "")
|
|
|
|
|
email = data.get("email", "")
|
|
|
|
|
new_items = data.get("items", [])
|
|
|
|
|
if not week or not email:
|
|
|
|
|
return jsonify({"error": "week and email are required"}), 400
|
|
|
|
|
|
|
|
|
|
slug = email.strip().lower().replace("/", "_").replace("\\", "_")
|
|
|
|
|
order_file = ORDERS_DIR / week / f"{slug}.json"
|
|
|
|
|
if not order_file.exists():
|
|
|
|
|
return jsonify({"error": "Order not found"}), 404
|
|
|
|
|
|
|
|
|
|
with open(order_file) as f:
|
|
|
|
|
existing = json.load(f)
|
|
|
|
|
|
|
|
|
|
filtered = [i for i in new_items if i.get("quantity", 0) > 0]
|
|
|
|
|
if not filtered:
|
|
|
|
|
return jsonify({"error": "At least one item required"}), 400
|
|
|
|
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
TWO_PLACES = Decimal("0.01")
|
|
|
|
|
bulk_pct = Decimal(str(config.get("bulk_discount_percent", 0)))
|
|
|
|
|
subsidy_pct = Decimal(str(config.get("company_subsidy_percent", 0)))
|
|
|
|
|
bulk_mult = Decimal("1") - (
|
|
|
|
|
max(Decimal("0"), min(Decimal("100"), bulk_pct)) / Decimal("100")
|
|
|
|
|
)
|
|
|
|
|
subsidy_mult = Decimal("1") - (
|
|
|
|
|
max(Decimal("0"), min(Decimal("100"), subsidy_pct)) / Decimal("100")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
official_retail = _official_menu_retail_by_name()
|
|
|
|
|
for item in filtered:
|
|
|
|
|
meal_name = (item.get("name") or "").strip()
|
|
|
|
|
retail = official_retail.get(meal_name)
|
|
|
|
|
if retail is None:
|
|
|
|
|
return jsonify({"error": f"'{meal_name}' not on this week's menu"}), 400
|
|
|
|
|
qty = Decimal(str(item["quantity"]))
|
|
|
|
|
bulk_price = (retail * bulk_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
|
|
|
|
emp_price = (bulk_price * subsidy_mult).quantize(
|
|
|
|
|
TWO_PLACES, rounding=ROUND_HALF_UP
|
|
|
|
|
)
|
|
|
|
|
subtotal = (emp_price * qty).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
|
|
|
|
item["retail_price"] = float(retail)
|
|
|
|
|
item["bulk_price"] = float(bulk_price)
|
|
|
|
|
item["price"] = float(emp_price)
|
|
|
|
|
item["subtotal"] = float(subtotal)
|
|
|
|
|
|
|
|
|
|
total = float(
|
|
|
|
|
sum(Decimal(str(i["subtotal"])) for i in filtered).quantize(
|
|
|
|
|
TWO_PLACES, rounding=ROUND_HALF_UP
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
existing["items"] = filtered
|
|
|
|
|
existing["total"] = total
|
|
|
|
|
|
|
|
|
|
with open(order_file, "w") as f:
|
|
|
|
|
json.dump(existing, f, indent=2)
|
|
|
|
|
|
|
|
|
|
return jsonify({"status": "updated", "week": week, "email": email, "total": total})
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
@app.route("/api/orders/<week>")
|
|
|
|
|
def get_orders(week: str):
|
|
|
|
|
week_dir = ORDERS_DIR / week
|
|
|
|
|
if not week_dir.exists():
|
|
|
|
|
return jsonify({"orders": [], "week": week})
|
|
|
|
|
|
|
|
|
|
orders = []
|
|
|
|
|
for f in sorted(week_dir.glob("*.json")):
|
|
|
|
|
with open(f) as fh:
|
|
|
|
|
orders.append(json.load(fh))
|
|
|
|
|
|
|
|
|
|
return jsonify({"orders": orders, "week": week})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
ORDERS_DIR.mkdir(exist_ok=True)
|
|
|
|
|
print(f"Menu file: {latest_menu_file()}")
|
|
|
|
|
print(f"Orders dir: {ORDERS_DIR}")
|
2026-07-23 16:00:47 -04:00
|
|
|
app.run(host="0.0.0.0", port=5050, debug=False)
|