meal-order-manager/functions/aggregate_orders/handler.py

137 lines
4.1 KiB
Python
Raw Normal View History

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()