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.
This commit is contained in:
Adam Moussa 2026-05-13 11:35:56 -04:00
parent e6bef44a74
commit 81543c3b7f
3 changed files with 222 additions and 18 deletions

View file

@ -22,6 +22,8 @@ def lambda_handler(event, context):
return handle_orders_aggregated(event)
elif event_type == "reminder":
return handle_reminder(event)
elif event_type == "order_confirmed":
return handle_order_confirmed(event)
return {"error": f"Unknown event type: {event_type}"}
@ -31,7 +33,7 @@ def handle_menu_published(event):
week = event.get("week", current_week())
meal_count = event.get("meal_count", "")
text = "This week's meal order is open! Deadline: Thursday 6pm."
text = "This week's meal order is open! Deadline: Thursday at 11:59pm."
blocks = [
{
"type": "header",
@ -43,7 +45,7 @@ def handle_menu_published(event):
"type": "mrkdwn",
"text": (
f"*<{form_url}|Place your order>*\n\n"
f"*Deadline:* Thursday 6pm\n"
f"*Deadline:* Thursday at 11:59pm\n"
f"*Menu:* {meal_count} meals available"
),
},
@ -63,12 +65,20 @@ def handle_orders_aggregated(event):
total_employees = int(summary.get("total_employees", 0))
total_meals = int(summary.get("total_meals", 0))
grand_total = float(summary.get("grand_total", 0))
employee_total = float(summary.get("employee_total", grand_total))
meal_lines = []
for m in summary.get("meals", []):
meal_lines.append(f"{m['meal']}: *{int(m['quantity'])}*")
meal_list = "\n".join(meal_lines)
has_subsidy = employee_total < grand_total
totals_text = (
f"*${grand_total:.2f}* order total (bulk rate)\n"
f"*${employee_total:.2f}* payroll deductions"
+ (f"\n*${grand_total - employee_total:.2f}* company subsidy" if has_subsidy else "")
)
text = f"Meal orders closed for {week}. {total_employees} employees, {total_meals} meals, ${grand_total:.2f} total."
blocks = [
{
@ -82,7 +92,7 @@ def handle_orders_aggregated(event):
"text": (
f"*{total_employees}* employees ordered\n"
f"*{total_meals}* total meals\n"
f"*${grand_total:.2f}* grand total"
f"{totals_text}"
),
},
},
@ -100,6 +110,50 @@ def handle_orders_aggregated(event):
return {"status": "notified", "event": "orders_aggregated", "week": week}
def handle_order_confirmed(event):
email = event.get("employee_email", "").lower()
name = event.get("employee_name", "")
items = event.get("items", [])
total = float(event.get("total", 0))
week = event.get("week", current_week())
roster = get_roster()
employee = next((e for e in roster if e["email"].lower() == email), None)
if not employee or not employee.get("slack_user_id"):
return {"status": "no_slack_id", "email": email}
item_lines = []
for item in items:
qty = int(item.get("quantity", 0))
price = float(item.get("price", 0))
item_lines.append(f"{item['name']} x{qty} — ${price * qty:.2f}")
item_list = "\n".join(item_lines)
send_dm(
employee["slack_user_id"],
f"Order confirmed for {week}: ${total:.2f} total.",
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": f"Order Confirmed — {week}"},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"Hey {name.split()[0]}! Your meal order has been submitted.\n\n"
f"{item_list}\n\n"
f"*Total: ${total:.2f}*"
),
},
},
],
)
return {"status": "confirmed", "week": week, "employee": name}
def handle_reminder(event):
week = event.get("week", current_week())
form_url = os.environ.get("FORM_URL", "")
@ -120,14 +174,14 @@ def handle_reminder(event):
continue
send_dm(
slack_id,
f"Meal orders close at 6pm today. Place your order: {form_url}",
f"Meal orders close today at 11:59pm. Place your order: {form_url}",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"Hey {emp['name'].split()[0]}! Meal orders close at *6pm today*.\n\n"
f"Hey {emp['name'].split()[0]}! Meal orders close today at *11:59pm*.\n\n"
f"*<{form_url}|Place your order>*"
),
},

View file

@ -1,13 +1,19 @@
import json
import os
import urllib.request
from datetime import datetime
from zoneinfo import ZoneInfo
from shared.db import current_week, get_form_status, get_roster, put_order
from shared.secrets import get_secret
import boto3
from shared.db import current_week, get_form_status, get_roster, get_settings, put_order
from shared.secrets import get_parameter, get_secret
EASTERN = ZoneInfo("America/New_York")
_api_key = None
_settings = None
_google_client_id = None
_lambda = boto3.client("lambda")
def _get_api_key() -> str:
@ -17,6 +23,47 @@ def _get_api_key() -> str:
return _api_key
def _get_discount_settings() -> tuple[float, float]:
global _settings
if _settings is None:
s = get_settings()
_settings = (
float(s.get("bulk_discount_percent", 0)),
float(s.get("company_subsidy_percent", 0)),
)
return _settings
def _get_google_client_id() -> str:
global _google_client_id
if _google_client_id is None:
param = os.environ.get("GOOGLE_CLIENT_ID_PARAM", "")
if param:
_google_client_id = get_parameter(param, decrypt=False) or ""
else:
_google_client_id = ""
return _google_client_id
def _verify_google_token(token: str) -> dict | None:
client_id = _get_google_client_id()
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
def lambda_handler(event, context):
method = event.get("requestContext", {}).get("http", {}).get("method", "GET")
path = event.get("rawPath", "")
@ -55,8 +102,17 @@ def handle_submit(event):
except json.JSONDecodeError:
return response(400, {"error": "Invalid JSON"})
name = body.get("employee_name", "").strip()
email = body.get("employee_email", "").strip()
google_token = body.get("google_id_token")
if google_token:
user_info = _verify_google_token(google_token)
if not user_info:
return response(403, {"error": "Invalid or unauthorized Google account"})
name = user_info["name"]
email = user_info["email"]
else:
name = body.get("employee_name", "").strip()
email = body.get("employee_email", "").strip()
items = body.get("items", [])
if not name:
@ -74,7 +130,22 @@ def handle_submit(event):
return response(404, {"error": "No menu available for this week"})
filtered_items = [i for i in items if i.get("quantity", 0) > 0]
total = sum((i.get("price", 0) or 0) * i.get("quantity", 0) for i in filtered_items)
bulk_pct, subsidy_pct = _get_discount_settings()
bulk_mult = 1 - (bulk_pct / 100)
subsidy_mult = 1 - (subsidy_pct / 100)
for item in filtered_items:
retail = item.get("retail_price", item.get("price", 0)) or 0
qty = item.get("quantity", 0)
bulk_price = round(retail * bulk_mult, 2)
emp_price = round(bulk_price * subsidy_mult, 2)
item["retail_price"] = retail
item["bulk_price"] = bulk_price
item["price"] = emp_price
item["subtotal"] = round(emp_price * qty, 2)
total = sum(i["subtotal"] for i in filtered_items)
slug = (
"".join(c if c.isalnum() or c in "- " else "" for c in name)
@ -93,6 +164,19 @@ def handle_submit(event):
put_order(week, slug, order_data)
_lambda.invoke(
FunctionName=os.environ["SLACK_NOTIFIER_ARN"],
InvocationType="Event",
Payload=json.dumps({
"event": "order_confirmed",
"employee_name": name,
"employee_email": email,
"items": filtered_items,
"total": order_data["total"],
"week": week,
}, default=float),
)
return response(
200,
{

View file

@ -6,9 +6,11 @@ Orders are saved as JSON files in the orders directory, one per employee per wee
"""
import json
import urllib.request
from datetime import datetime
from pathlib import Path
import boto3
from flask import Flask, jsonify, request, send_file
PROJECT_ROOT = Path(__file__).resolve().parents[2]
@ -58,14 +60,58 @@ def get_roster():
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
name = data.get("employee_name", "").strip()
email = data.get("employee_email", "").strip()
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:
@ -75,6 +121,23 @@ def submit_order():
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()
bulk_pct = config.get("bulk_discount_percent", 0)
subsidy_pct = config.get("company_subsidy_percent", 0)
bulk_mult = 1 - (bulk_pct / 100)
subsidy_mult = 1 - (subsidy_pct / 100)
filtered = [i for i in items if i.get("quantity", 0) > 0]
for item in filtered:
retail = item.get("retail_price", item.get("price", 0)) or 0
qty = item.get("quantity", 0)
bulk_price = round(retail * bulk_mult, 2)
emp_price = round(bulk_price * subsidy_mult, 2)
item["retail_price"] = retail
item["bulk_price"] = bulk_price
item["price"] = emp_price
item["subtotal"] = round(emp_price * qty, 2)
week = current_week()
week_dir = ORDERS_DIR / week
week_dir.mkdir(parents=True, exist_ok=True)
@ -87,17 +150,15 @@ def submit_order():
)
order_file = week_dir / f"{safe_name}.json"
total = round(sum(i["subtotal"] for i in filtered), 2)
order = {
"employee_name": name,
"employee_email": email,
"week": week,
"submitted_at": datetime.now().isoformat(),
"items": [i for i in items if i.get("quantity", 0) > 0],
"total": sum(
(i.get("price", 0) or 0) * i.get("quantity", 0)
for i in items
if i.get("quantity", 0) > 0
),
"items": filtered,
"total": total,
}
with open(order_file, "w") as f:
@ -108,6 +169,11 @@ def submit_order():
)
@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