mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 20:33:13 +00:00
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.
198 lines
6 KiB
Python
198 lines
6 KiB
Python
import json
|
|
import os
|
|
from decimal import Decimal
|
|
|
|
from shared.db import current_week, get_orders, get_roster, get_summary
|
|
from shared.slack import post_channel_message, send_dm
|
|
|
|
|
|
class DecimalEncoder(json.JSONEncoder):
|
|
def default(self, o):
|
|
if isinstance(o, Decimal):
|
|
return float(o)
|
|
return super().default(o)
|
|
|
|
|
|
def lambda_handler(event, context):
|
|
event_type = event.get("event", "")
|
|
|
|
if event_type == "menu_published":
|
|
return handle_menu_published(event)
|
|
elif event_type == "orders_aggregated":
|
|
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}"}
|
|
|
|
|
|
def handle_menu_published(event):
|
|
form_url = os.environ.get("FORM_URL", "")
|
|
week = event.get("week", current_week())
|
|
meal_count = event.get("meal_count", "")
|
|
|
|
text = "This week's meal order is open! Deadline: Thursday at 11:59pm."
|
|
blocks = [
|
|
{
|
|
"type": "header",
|
|
"text": {"type": "plain_text", "text": "Meal Order Open"},
|
|
},
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "mrkdwn",
|
|
"text": (
|
|
f"*<{form_url}|Place your order>*\n\n"
|
|
f"*Deadline:* Thursday at 11:59pm\n"
|
|
f"*Menu:* {meal_count} meals available"
|
|
),
|
|
},
|
|
},
|
|
]
|
|
|
|
post_channel_message(text, blocks)
|
|
return {"status": "notified", "event": "menu_published", "week": week}
|
|
|
|
|
|
def handle_orders_aggregated(event):
|
|
week = event.get("week", current_week())
|
|
summary = get_summary(week)
|
|
if not summary:
|
|
return {"status": "no_summary", "week": week}
|
|
|
|
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 = [
|
|
{
|
|
"type": "header",
|
|
"text": {"type": "plain_text", "text": f"Meal Orders Closed — {week}"},
|
|
},
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "mrkdwn",
|
|
"text": (
|
|
f"*{total_employees}* employees ordered\n"
|
|
f"*{total_meals}* total meals\n"
|
|
f"{totals_text}"
|
|
),
|
|
},
|
|
},
|
|
{"type": "divider"},
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "mrkdwn",
|
|
"text": f"*Order for Redefine Meals:*\n{meal_list}",
|
|
},
|
|
},
|
|
]
|
|
|
|
post_channel_message(text, blocks)
|
|
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", "")
|
|
|
|
roster = get_roster()
|
|
if not roster:
|
|
return {"status": "no_roster"}
|
|
|
|
orders = get_orders(week)
|
|
ordered_emails = {o["employee_email"].lower() for o in orders}
|
|
|
|
missing = [emp for emp in roster if emp["email"].lower() not in ordered_emails]
|
|
|
|
dm_count = 0
|
|
for emp in missing:
|
|
slack_id = emp.get("slack_user_id")
|
|
if not slack_id:
|
|
continue
|
|
send_dm(
|
|
slack_id,
|
|
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 today at *11:59pm*.\n\n"
|
|
f"*<{form_url}|Place your order>*"
|
|
),
|
|
},
|
|
}
|
|
],
|
|
)
|
|
dm_count += 1
|
|
|
|
return {
|
|
"status": "reminders_sent",
|
|
"week": week,
|
|
"dm_count": dm_count,
|
|
"missing_count": len(missing),
|
|
}
|