meal-order-manager/src/server/app.py
Adam Moussa 5c040bf55c 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)
2026-05-13 13:39:18 -04:00

204 lines
6.1 KiB
Python

"""
Lightweight Flask server for the meal order form.
Serves the generated HTML form and handles order submissions.
Orders are saved as JSON files in the orders directory, one per employee per week.
"""
import json
import urllib.request
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
import boto3
from flask import Flask, jsonify, request, send_file
PROJECT_ROOT = Path(__file__).resolve().parents[2]
CONFIG_PATH = PROJECT_ROOT / "config.json"
OUTPUT_DIR = PROJECT_ROOT / "output"
ORDERS_DIR = PROJECT_ROOT / "orders"
app = Flask(__name__)
def load_config():
with open(CONFIG_PATH) as f:
return json.load(f)
def current_week() -> str:
return datetime.now().strftime("%Y-W%U")
def latest_menu_file() -> Path | None:
files = sorted(OUTPUT_DIR.glob("menu-*.json"), reverse=True)
return files[0] if files else None
@app.route("/")
def index():
form_file = OUTPUT_DIR / f"order-form-{current_week()}.html"
if not form_file.exists():
return "No order form generated for this week. Run generate_form.py first.", 404
return send_file(form_file)
@app.route("/api/menu")
def get_menu():
menu_file = latest_menu_file()
if not menu_file:
return jsonify(
{"error": "No menu data available. Run scrape_menu.py first."}
), 404
with open(menu_file) as f:
return jsonify(json.load(f))
@app.route("/api/roster")
def get_roster():
config = load_config()
return jsonify(config.get("roster", []))
_google_client_id_cache = None
def _get_google_client_id() -> str:
global _google_client_id_cache
if _google_client_id_cache is None:
config = load_config()
_google_client_id_cache = config.get("google_client_id", "")
if not _google_client_id_cache:
try:
ssm = boto3.client("ssm")
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
_google_client_id_cache = resp["Parameter"]["Value"]
except Exception:
_google_client_id_cache = ""
return _google_client_id_cache
def _verify_google_token(token: str, client_id: str) -> dict | None:
if not client_id:
return None
try:
req = urllib.request.Request(
f"https://oauth2.googleapis.com/tokeninfo?id_token={token}"
)
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read())
if data.get("aud") != client_id:
return None
return {"name": data.get("name", ""), "email": data.get("email", "")}
except Exception:
return None
@app.route("/api/submit-order", methods=["POST"])
def submit_order():
data = request.get_json()
if not data:
return jsonify({"error": "No data received"}), 400
google_token = data.get("google_id_token")
if google_token:
client_id = _get_google_client_id()
user_info = _verify_google_token(google_token, client_id)
if not user_info:
return jsonify({"error": "Invalid or unauthorized Google account"}), 403
name = user_info["name"]
email = user_info["email"]
else:
name = data.get("employee_name", "").strip()
email = data.get("employee_email", "").strip()
items = data.get("items", [])
if not name:
return jsonify({"error": "Employee name is required"}), 400
if not email:
return jsonify({"error": "Employee email is required"}), 400
if not items or not any(i.get("quantity", 0) > 0 for i in items):
return jsonify({"error": "Please select at least one meal"}), 400
config = load_config()
TWO_PLACES = Decimal("0.01")
bulk_pct = Decimal(str(config.get("bulk_discount_percent", 0)))
subsidy_pct = Decimal(str(config.get("company_subsidy_percent", 0)))
bulk_mult = Decimal("1") - (bulk_pct / Decimal("100"))
subsidy_mult = Decimal("1") - (subsidy_pct / Decimal("100"))
filtered = [i for i in items if i.get("quantity", 0) > 0]
for item in filtered:
retail = Decimal(str(item.get("retail_price", item.get("price", 0)) or 0))
qty = Decimal(str(item.get("quantity", 0)))
bulk_price = (retail * bulk_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
emp_price = (bulk_price * subsidy_mult).quantize(
TWO_PLACES, rounding=ROUND_HALF_UP
)
subtotal = (emp_price * qty).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
item["retail_price"] = float(retail)
item["bulk_price"] = float(bulk_price)
item["price"] = float(emp_price)
item["subtotal"] = float(subtotal)
week = current_week()
week_dir = ORDERS_DIR / week
week_dir.mkdir(parents=True, exist_ok=True)
safe_name = (
"".join(c if c.isalnum() or c in "-_ " else "" for c in name)
.strip()
.replace(" ", "-")
.lower()
)
order_file = week_dir / f"{safe_name}.json"
total = float(
sum(Decimal(str(i["subtotal"])) for i in filtered).quantize(
TWO_PLACES, rounding=ROUND_HALF_UP
)
)
order = {
"employee_name": name,
"employee_email": email,
"week": week,
"submitted_at": datetime.now().isoformat(),
"items": filtered,
"total": total,
}
with open(order_file, "w") as f:
json.dump(order, f, indent=2)
return jsonify(
{"status": "ok", "message": f"Order saved for {name}", "total": order["total"]}
)
@app.route("/api/form-status/<week>")
def form_status(week: str):
return jsonify({"week": week, "status": "open"})
@app.route("/api/orders/<week>")
def get_orders(week: str):
week_dir = ORDERS_DIR / week
if not week_dir.exists():
return jsonify({"orders": [], "week": week})
orders = []
for f in sorted(week_dir.glob("*.json")):
with open(f) as fh:
orders.append(json.load(fh))
return jsonify({"orders": orders, "week": week})
if __name__ == "__main__":
ORDERS_DIR.mkdir(exist_ok=True)
print(f"Menu file: {latest_menu_file()}")
print(f"Orders dir: {ORDERS_DIR}")
app.run(host="0.0.0.0", port=5050, debug=True)