mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 13:33:13 +00:00
- 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)
204 lines
6.1 KiB
Python
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)
|