Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
"""Unit tests for the slack_notifier handler."""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Ensure the handler module can be imported. The shared layer lives under
|
2026-09-21 19:34:24 +00:00
|
|
|
# src/shared/ and the handler lives under src/server/jobs/.
|
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 importlib.util
|
|
|
|
|
|
|
|
|
|
_repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
sys.path.insert(0, os.path.join(_repo, "src", "shared"))
|
|
|
|
|
|
2026-09-21 19:34:24 +00:00
|
|
|
_handler_path = os.path.join(_repo, "src", "server", "jobs", "notify.py")
|
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
|
|
|
_spec = importlib.util.spec_from_file_location("slack_notifier_handler", _handler_path)
|
|
|
|
|
handler = importlib.util.module_from_spec(_spec)
|
|
|
|
|
sys.modules["slack_notifier_handler"] = handler
|
|
|
|
|
_spec.loader.exec_module(handler)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
2026-09-21 19:34:24 +00:00
|
|
|
# Reminder delayed SQS delivery
|
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
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 19:34:24 +00:00
|
|
|
class TestReminderDelayedDelivery:
|
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
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_orders", return_value=[])
|
|
|
|
|
@patch("slack_notifier_handler.get_roster", return_value=[])
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
2026-09-21 19:34:24 +00:00
|
|
|
def test_reminder_runs_after_scheduled_hour(
|
|
|
|
|
self, mock_week, mock_roster, mock_orders, mock_dm
|
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
|
|
|
):
|
|
|
|
|
result = handler.handle_reminder({"event": "reminder"})
|
|
|
|
|
|
2026-09-21 19:34:24 +00:00
|
|
|
assert result["status"] != "skipped"
|
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
|
|
|
|
2026-09-21 19:34:24 +00:00
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_orders", return_value=[])
|
|
|
|
|
@patch("slack_notifier_handler.get_roster", return_value=[])
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_lambda_handler_reminder_does_not_skip(
|
|
|
|
|
self, mock_week, mock_roster, mock_orders, mock_dm
|
|
|
|
|
):
|
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
|
|
|
result = handler.lambda_handler({"event": "reminder"}, None)
|
|
|
|
|
|
2026-09-21 19:34:24 +00:00
|
|
|
assert result["status"] != "skipped"
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Reminder DMs (High)
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReminderDMs:
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_orders")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_reminder_sends_to_non_ordered(
|
2026-09-21 19:34:24 +00:00
|
|
|
self, mock_week, mock_roster, mock_orders, mock_dm
|
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
|
|
|
):
|
|
|
|
|
"""Roster of 3, 1 has ordered -> DMs sent to 2 others."""
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "alice@x.com", "name": "Alice A", "slack_user_id": "U001"},
|
|
|
|
|
{"email": "bob@x.com", "name": "Bob B", "slack_user_id": "U002"},
|
|
|
|
|
{"email": "carol@x.com", "name": "Carol C", "slack_user_id": "U003"},
|
|
|
|
|
]
|
|
|
|
|
mock_orders.return_value = [
|
|
|
|
|
{"employee_email": "alice@x.com"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = handler.handle_reminder({"event": "reminder"})
|
|
|
|
|
|
|
|
|
|
assert result["dm_count"] == 2, "Should DM the 2 employees who haven't ordered"
|
|
|
|
|
assert result["missing_count"] == 2
|
|
|
|
|
assert mock_dm.call_count == 2
|
|
|
|
|
|
|
|
|
|
dm_user_ids = {call.args[0] for call in mock_dm.call_args_list}
|
|
|
|
|
assert dm_user_ids == {"U002", "U003"}, "Should DM Bob and Carol, not Alice"
|
|
|
|
|
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_orders", return_value=[])
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_reminder_skips_no_slack_id(
|
2026-09-21 19:34:24 +00:00
|
|
|
self, mock_week, mock_roster, mock_orders, mock_dm
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
):
|
|
|
|
|
"""Employee without slack_user_id is counted as missing but not DM'd."""
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "dave@x.com", "name": "Dave D"}, # no slack_user_id
|
|
|
|
|
{"email": "eve@x.com", "name": "Eve E", "slack_user_id": "U005"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = handler.handle_reminder({"event": "reminder"})
|
|
|
|
|
|
|
|
|
|
assert result["missing_count"] == 2, "Both are missing (no orders at all)"
|
|
|
|
|
assert result["dm_count"] == 1, "Only Eve should be DM'd (Dave has no Slack ID)"
|
|
|
|
|
mock_dm.assert_called_once()
|
|
|
|
|
assert mock_dm.call_args.args[0] == "U005"
|
|
|
|
|
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_orders")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_reminder_case_insensitive_email(
|
2026-09-21 19:34:24 +00:00
|
|
|
self, mock_week, mock_roster, mock_orders, mock_dm
|
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
|
|
|
):
|
|
|
|
|
"""'Adam@x.com' in roster matches 'adam@x.com' in orders."""
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "Adam@x.com", "name": "Adam M", "slack_user_id": "U010"},
|
|
|
|
|
]
|
|
|
|
|
mock_orders.return_value = [
|
|
|
|
|
{"employee_email": "adam@x.com"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = handler.handle_reminder({"event": "reminder"})
|
|
|
|
|
|
|
|
|
|
assert result["dm_count"] == 0, (
|
|
|
|
|
"Adam already ordered (case-insensitive match) — no DM expected"
|
|
|
|
|
)
|
|
|
|
|
mock_dm.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Order Confirmed (High)
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestOrderConfirmed:
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_order_confirmed_dm_format(self, mock_week, mock_roster, mock_dm):
|
|
|
|
|
"""DM contains item breakdown with 'your cost' labels and payroll total."""
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "alice@x.com", "name": "Alice Adams", "slack_user_id": "U001"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
event = {
|
|
|
|
|
"event": "order_confirmed",
|
|
|
|
|
"employee_email": "alice@x.com",
|
|
|
|
|
"employee_name": "Alice Adams",
|
|
|
|
|
"items": [
|
|
|
|
|
{"name": "Grilled Chicken", "quantity": 2, "price": 8.50},
|
|
|
|
|
{"name": "Caesar Salad", "quantity": 1, "price": 6.00},
|
|
|
|
|
],
|
|
|
|
|
"total": 23.00,
|
|
|
|
|
"week": "2026-W19",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler.handle_order_confirmed(event)
|
|
|
|
|
|
|
|
|
|
mock_dm.assert_called_once()
|
|
|
|
|
call_kwargs = mock_dm.call_args
|
|
|
|
|
blocks = call_kwargs.kwargs.get("blocks") or call_kwargs[1].get("blocks")
|
|
|
|
|
body_text = blocks[1]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
assert "your cost: $17.00" in body_text, "Should show Grilled Chicken x2 cost"
|
|
|
|
|
assert "your cost: $6.00" in body_text, "Should show Caesar Salad x1 cost"
|
|
|
|
|
assert "$23.00" in body_text, "Should show payroll deduction total"
|
|
|
|
|
assert "payroll deduction" in body_text.lower()
|
|
|
|
|
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_order_confirmed_no_slack_id(self, mock_week, mock_roster, mock_dm):
|
|
|
|
|
"""Employee not in roster -> returns {'status': 'no_slack_id'}."""
|
|
|
|
|
mock_roster.return_value = [] # empty roster
|
|
|
|
|
|
|
|
|
|
event = {
|
|
|
|
|
"event": "order_confirmed",
|
|
|
|
|
"employee_email": "nobody@x.com",
|
|
|
|
|
"employee_name": "Nobody",
|
|
|
|
|
"items": [],
|
|
|
|
|
"total": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = handler.handle_order_confirmed(event)
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "no_slack_id"
|
|
|
|
|
mock_dm.assert_not_called()
|
|
|
|
|
|
|
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_order_confirmed_empty_name(self, mock_week, mock_roster, mock_dm):
|
|
|
|
|
"""Empty name -> greeting says 'Hey there!' not crash."""
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "anon@x.com", "name": "", "slack_user_id": "U099"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
event = {
|
|
|
|
|
"event": "order_confirmed",
|
|
|
|
|
"employee_email": "anon@x.com",
|
|
|
|
|
"employee_name": "",
|
|
|
|
|
"items": [{"name": "Soup", "quantity": 1, "price": 5.00}],
|
|
|
|
|
"total": 5.00,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = handler.handle_order_confirmed(event)
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "confirmed", "Should not crash on empty name"
|
|
|
|
|
|
|
|
|
|
blocks = mock_dm.call_args.kwargs.get("blocks") or mock_dm.call_args[1].get(
|
|
|
|
|
"blocks"
|
|
|
|
|
)
|
|
|
|
|
body_text = blocks[1]["text"]["text"]
|
|
|
|
|
assert "Hey there!" in body_text, (
|
|
|
|
|
"Should greet with 'Hey there!' when name is empty"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Orders Aggregated (High)
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestOrdersAggregated:
|
2026-07-08 16:33:42 -04:00
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.get_settings")
|
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
|
|
|
@patch("slack_notifier_handler.get_summary")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
2026-07-08 16:33:42 -04:00
|
|
|
def test_orders_aggregated_with_subsidy(
|
|
|
|
|
self, mock_week, mock_summary, mock_settings, mock_roster, mock_dm
|
|
|
|
|
):
|
|
|
|
|
"""employee_total < grand_total -> admin DM includes company subsidy line."""
|
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
|
|
|
mock_summary.return_value = {
|
|
|
|
|
"total_employees": 5,
|
|
|
|
|
"total_meals": 12,
|
|
|
|
|
"grand_total": Decimal("150.00"),
|
|
|
|
|
"employee_total": Decimal("120.00"),
|
|
|
|
|
"meals": [
|
|
|
|
|
{"meal": "Grilled Chicken", "quantity": Decimal("7")},
|
|
|
|
|
{"meal": "Veggie Bowl", "quantity": Decimal("5")},
|
|
|
|
|
],
|
|
|
|
|
}
|
2026-07-08 16:33:42 -04:00
|
|
|
mock_settings.return_value = {"admin_emails": ["admin@x.com"]}
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"},
|
|
|
|
|
]
|
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
|
|
|
|
|
|
|
|
result = handler.handle_orders_aggregated({"event": "orders_aggregated"})
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "notified"
|
2026-07-08 16:33:42 -04:00
|
|
|
mock_dm.assert_called_once()
|
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
|
|
|
|
2026-07-08 16:33:42 -04:00
|
|
|
blocks = mock_dm.call_args.args[2]
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
section_text = blocks[1]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
assert "$150.00" in section_text, "Should show grand total"
|
|
|
|
|
assert "$120.00" in section_text, "Should show employee payroll deductions"
|
|
|
|
|
assert "$30.00" in section_text, "Should show company subsidy amount"
|
|
|
|
|
assert "company subsidy" in section_text.lower()
|
|
|
|
|
|
2026-07-08 16:33:42 -04:00
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.get_settings")
|
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
|
|
|
@patch("slack_notifier_handler.get_summary")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_orders_aggregated_without_subsidy(
|
2026-07-08 16:33:42 -04:00
|
|
|
self, mock_week, mock_summary, mock_settings, mock_roster, mock_dm
|
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
|
|
|
):
|
|
|
|
|
"""Equal totals -> no subsidy line."""
|
|
|
|
|
mock_summary.return_value = {
|
|
|
|
|
"total_employees": 3,
|
|
|
|
|
"total_meals": 6,
|
|
|
|
|
"grand_total": Decimal("90.00"),
|
|
|
|
|
"employee_total": Decimal("90.00"),
|
|
|
|
|
"meals": [
|
|
|
|
|
{"meal": "Pasta Primavera", "quantity": Decimal("6")},
|
|
|
|
|
],
|
|
|
|
|
}
|
2026-07-08 16:33:42 -04:00
|
|
|
mock_settings.return_value = {"admin_emails": ["admin@x.com"]}
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"},
|
|
|
|
|
]
|
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
|
|
|
|
|
|
|
|
result = handler.handle_orders_aggregated({"event": "orders_aggregated"})
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "notified"
|
2026-07-08 16:33:42 -04:00
|
|
|
blocks = mock_dm.call_args.args[2]
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
section_text = blocks[1]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
assert "company subsidy" not in section_text.lower(), (
|
|
|
|
|
"Should not mention subsidy when employee_total == grand_total"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@patch("slack_notifier_handler.post_channel_message")
|
|
|
|
|
@patch("slack_notifier_handler.get_summary")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_orders_aggregated_no_summary(self, mock_week, mock_summary, mock_post):
|
|
|
|
|
"""No summary in DB -> returns {'status': 'no_summary'}."""
|
|
|
|
|
mock_summary.return_value = None
|
|
|
|
|
|
|
|
|
|
result = handler.handle_orders_aggregated({"event": "orders_aggregated"})
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "no_summary"
|
|
|
|
|
mock_post.assert_not_called()
|
|
|
|
|
|
2026-07-08 16:33:42 -04:00
|
|
|
@patch("slack_notifier_handler.send_dm")
|
|
|
|
|
@patch("slack_notifier_handler.get_roster")
|
|
|
|
|
@patch("slack_notifier_handler.get_settings")
|
|
|
|
|
@patch("slack_notifier_handler.post_channel_message")
|
|
|
|
|
@patch("slack_notifier_handler.get_summary")
|
|
|
|
|
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
|
|
|
|
def test_orders_aggregated_dms_admins(
|
|
|
|
|
self, mock_week, mock_summary, mock_post, mock_settings, mock_roster, mock_dm
|
|
|
|
|
):
|
|
|
|
|
"""Admins with a Slack ID get DM'd the summary; others are skipped."""
|
|
|
|
|
mock_summary.return_value = {
|
|
|
|
|
"total_employees": 2,
|
|
|
|
|
"total_meals": 4,
|
|
|
|
|
"grand_total": Decimal("60.00"),
|
|
|
|
|
"employee_total": Decimal("60.00"),
|
|
|
|
|
"meals": [
|
|
|
|
|
{"meal": "Grilled Chicken", "quantity": Decimal("4")},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
mock_settings.return_value = {
|
|
|
|
|
"admin_emails": ["Admin@x.com", "noslack@x.com", "missing@x.com"],
|
|
|
|
|
}
|
|
|
|
|
mock_roster.return_value = [
|
|
|
|
|
{"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"},
|
|
|
|
|
{"email": "noslack@x.com", "name": "No Slack"}, # no slack_user_id
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = handler.handle_orders_aggregated({"event": "orders_aggregated"})
|
|
|
|
|
|
|
|
|
|
assert result["status"] == "notified"
|
|
|
|
|
assert result["admin_dm_count"] == 1, "Only the admin with a Slack ID is DM'd"
|
|
|
|
|
mock_dm.assert_called_once()
|
|
|
|
|
assert mock_dm.call_args.args[0] == "U100"
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Escaping (Low)
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEscaping:
|
|
|
|
|
def test_escape_mrkdwn(self):
|
|
|
|
|
"""'A & B <C>' -> 'A & B <C>'."""
|
|
|
|
|
assert handler._escape_mrkdwn("A & B <C>") == "A & B <C>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Routing
|
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRouting:
|
|
|
|
|
def test_unknown_event_type(self):
|
|
|
|
|
"""Unknown event type returns error message."""
|
|
|
|
|
result = handler.lambda_handler({"event": "bogus_event"}, None)
|
|
|
|
|
|
|
|
|
|
assert "error" in result, "Should return an error key for unknown event type"
|
|
|
|
|
assert "bogus_event" in result["error"], (
|
|
|
|
|
"Error message should include the unrecognised event type"
|
|
|
|
|
)
|