""" 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 time 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 def _official_menu_retail_by_name() -> dict[str, Decimal]: menu_file = latest_menu_file() if not menu_file: return {} with open(menu_file) as f: meals = (json.load(f) or {}).get("meals") or [] out: dict[str, Decimal] = {} for meal in meals: name = (meal.get("name") or "").strip() if not name or meal.get("price") is None: continue out[name] = Decimal(str(meal["price"])) return out @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", [])) # Match functions/submit_order/handler.py: TTL so a transient SSM failure cannot # pin client_id to "" for the process lifetime (which would skip Google auth). _GOOGLE_CLIENT_ID_CACHE_TTL_SECONDS = 300 _google_client_id_cache: str | None = None _google_client_id_cache_ts = 0.0 def _get_google_client_id() -> str: global _google_client_id_cache, _google_client_id_cache_ts now = time.monotonic() if ( _google_client_id_cache is not None and (now - _google_client_id_cache_ts) <= _GOOGLE_CLIENT_ID_CACHE_TTL_SECONDS ): return _google_client_id_cache config = load_config() from_config = (config.get("google_client_id") or "").strip() if from_config: _google_client_id_cache = from_config _google_client_id_cache_ts = now return _google_client_id_cache try: ssm = boto3.client("ssm") resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id") _google_client_id_cache = (resp["Parameter"].get("Value") or "").strip() except Exception: _google_client_id_cache = "" _google_client_id_cache_ts = now return _google_client_id_cache 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 if data.get("hd") != "seahavenind.com": 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 client_id = _get_google_client_id() google_token = data.get("google_id_token") if client_id: if not google_token: return jsonify({"error": "Google authentication is required"}), 403 user_info = _verify_google_token(google_token, client_id) if not user_info: return jsonify({"error": "Invalid or unauthorized Google account"}), 403 name = user_info["name"] email = user_info["email"] else: name = data.get("employee_name", "").strip() email = data.get("employee_email", "").strip() 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_pct = max(Decimal("0"), min(Decimal("100"), bulk_pct)) subsidy_pct = max(Decimal("0"), min(Decimal("100"), subsidy_pct)) bulk_mult = Decimal("1") - (bulk_pct / Decimal("100")) subsidy_mult = Decimal("1") - (subsidy_pct / Decimal("100")) filtered = [i for i in items if i.get("quantity", 0) > 0] official_retail = _official_menu_retail_by_name() if not official_retail: return jsonify({"error": "Menu temporarily unavailable"}), 503 for item in filtered: meal_name = (item.get("name") or "").strip() if meal_name not in official_retail: return jsonify({"error": "One or more meals are not on this week's menu"}), 400 for item in filtered: meal_name = (item.get("name") or "").strip() retail = official_retail[meal_name] qty = Decimal(str(item.get("quantity", 0))) bulk_price = (retail * bulk_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP) emp_price = (bulk_price * subsidy_mult).quantize( TWO_PLACES, rounding=ROUND_HALF_UP ) subtotal = (emp_price * qty).quantize(TWO_PLACES, rounding=ROUND_HALF_UP) item["retail_price"] = float(retail) item["bulk_price"] = float(bulk_price) item["price"] = float(emp_price) item["subtotal"] = float(subtotal) week = current_week() week_dir = ORDERS_DIR / week week_dir.mkdir(parents=True, exist_ok=True) # Match Lambda: one order file per employee email (not display name). slug = email.strip().lower() slug_safe = slug.replace("/", "_").replace("\\", "_") order_file = week_dir / f"{slug_safe}.json" total = float( sum(Decimal(str(i["subtotal"])) for i in filtered).quantize( TWO_PLACES, rounding=ROUND_HALF_UP ) ) 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/") def form_status(week: str): return jsonify({"week": week, "status": "open"}) @app.route("/api/orders/") 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)