import csv import io import json import logging 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 from shared.pdf import build_weekly_summary_pdf EASTERN = ZoneInfo("America/New_York") logger = logging.getLogger() logger.setLevel(logging.INFO) _s3 = boto3.client("s3") _lambda = boto3.client("lambda") _sqs = boto3.client("sqs") KIND_MEAL = "meal_deduction" 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) summary_pdf = build_weekly_summary_pdf( orders, week=week, generated_at=summary["generated_at"], total_employees=summary["total_employees"], total_meals=summary["total_meals"], ) bucket = os.environ["REPORTS_BUCKET"] order_csv_key = f"reports/{week}/order-summary.csv" payroll_csv_key = f"reports/{week}/payroll-deductions.csv" summary_pdf_key = f"reports/{week}/weekly-summary-{week}.pdf" _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" ) _s3.put_object( Bucket=bucket, Key=summary_pdf_key, Body=summary_pdf, ContentType="application/pdf", ) summary["order_csv_s3_key"] = order_csv_key summary["payroll_csv_s3_key"] = payroll_csv_key summary["weekly_summary_pdf_s3_key"] = summary_pdf_key put_summary(week, summary) try: _send_checkcomponents(week, orders) except Exception: logger.exception("checkcomponents send failed week=%s", week) _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_checkcomponents_payload(week: str, orders: list[dict]) -> dict: """Sibling dollar lines only. No Flex OAuth, no payPeriodId invention.""" lines = [] for order in orders: email = str(order.get("employee_email") or "").strip() if not email: continue amount = Decimal(str(order.get("total") or 0)) lines.append( { "employee_email": email, "amount": format(amount.quantize(Decimal("0.01")), "f"), } ) week_token = week if str(week).upper().startswith("WEEK#") else f"WEEK#{week}" return { "type": "checkcomponents", "kind": KIND_MEAL, "week": week_token, "lines": lines, } def _send_checkcomponents(week: str, orders: list[dict]) -> None: queue_url = os.environ.get("CHECKCOMPONENTS_QUEUE_URL", "").strip() if not queue_url: return payload = build_checkcomponents_payload(week, orders) if not payload["lines"]: return _sqs.send_message(QueueUrl=queue_url, MessageBody=json.dumps(payload)) def build_summary(orders: list[dict], week: str) -> dict: meal_totals = defaultdict( lambda: {"quantity": 0, "bulk_price": 0, "employee_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]["bulk_price"] = float( item.get("bulk_price", item.get("price", 0)) ) meal_totals[name]["employee_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["bulk_price"], "employee_unit_price": data["employee_price"], "line_total": round(data["bulk_price"] * data["quantity"], 2), "employee_line_total": round( data["employee_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), "employee_total": round(sum(m["employee_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()