meal-order-manager/functions/aggregate_orders/handler.py
Adam Moussa d332affd56
Initial commit: meal ordering automation system (#1)
Playwright-based menu scraper for Redefine Meals, self-contained HTML
order form with S3/CloudFront hosting, DynamoDB-backed order submission
via API Gateway, and automated payroll deduction reports via SES.
2026-05-12 18:25:15 -04:00

115 lines
3.8 KiB
Python

import csv
import io
import json
import os
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from zoneinfo import ZoneInfo
import boto3
from shared.db import current_week, get_orders, get_summary, put_summary
EASTERN = ZoneInfo("America/New_York")
_s3 = boto3.client("s3")
_lambda = boto3.client("lambda")
class DecimalEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Decimal):
return float(o)
return super().default(o)
def lambda_handler(event, context):
week = event.get("week", current_week())
existing = get_summary(week)
if existing:
return {"status": "already_aggregated", "week": week}
orders = get_orders(week)
if not orders:
return {"status": "no_orders", "week": week}
summary = build_summary(orders, week)
order_csv = build_order_summary_csv(summary)
payroll_csv = build_payroll_csv(orders)
bucket = os.environ["REPORTS_BUCKET"]
order_csv_key = f"reports/{week}/order-summary.csv"
payroll_csv_key = f"reports/{week}/payroll-deductions.csv"
_s3.put_object(Bucket=bucket, Key=order_csv_key, Body=order_csv, ContentType="text/csv")
_s3.put_object(Bucket=bucket, Key=payroll_csv_key, Body=payroll_csv, ContentType="text/csv")
summary["order_csv_s3_key"] = order_csv_key
summary["payroll_csv_s3_key"] = payroll_csv_key
put_summary(week, summary)
_lambda.invoke(
FunctionName=os.environ["SLACK_NOTIFIER_ARN"],
InvocationType="Event",
Payload=json.dumps({"event": "orders_aggregated", "week": week}, cls=DecimalEncoder),
)
return {"status": "aggregated", "week": week, "total_employees": len(orders)}
def build_summary(orders: list[dict], week: str) -> dict:
meal_totals = defaultdict(lambda: {"quantity": 0, "unit_price": 0})
for order in orders:
for item in order.get("items", []):
name = item["name"]
qty = int(item.get("quantity", 0))
meal_totals[name]["quantity"] += qty
meal_totals[name]["unit_price"] = float(item.get("price", 0))
meals = []
for name, data in sorted(meal_totals.items()):
meals.append({
"meal": name,
"quantity": data["quantity"],
"unit_price": data["unit_price"],
"line_total": round(data["unit_price"] * data["quantity"], 2),
})
return {
"week": week,
"generated_at": datetime.now(EASTERN).isoformat(),
"total_employees": len(orders),
"total_meals": sum(m["quantity"] for m in meals),
"grand_total": round(sum(m["line_total"] for m in meals), 2),
"meals": meals,
}
def build_order_summary_csv(summary: dict) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Meal", "Quantity", "Unit Price", "Line Total"])
for meal in summary["meals"]:
writer.writerow([meal["meal"], meal["quantity"], f"${meal['unit_price']:.2f}", f"${meal['line_total']:.2f}"])
writer.writerow([])
writer.writerow(["TOTAL", summary["total_meals"], "", f"${summary['grand_total']:.2f}"])
return buf.getvalue()
def build_payroll_csv(orders: list[dict]) -> str:
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["Employee Name", "Employee Email", "Items Ordered", "Total Deduction"])
for order in sorted(orders, key=lambda o: o["employee_name"]):
items_str = "; ".join(
f"{item['name']} x{int(item['quantity'])} (${float(item.get('subtotal', float(item.get('price', 0)) * int(item.get('quantity', 0)))):.2f})"
for item in order.get("items", [])
)
writer.writerow([
order["employee_name"],
order["employee_email"],
items_str,
f"${float(order['total']):.2f}",
])
return buf.getvalue()