import os import sqlite3 import json from datetime import datetime, timezone, timedelta EST = timezone(timedelta(hours=-5)) def to_est(dt_str): """Parse ISO timestamp and convert to EST.""" dt = datetime.fromisoformat(dt_str) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(EST) from collections import defaultdict import csv import io from flask import Flask, render_template_string, request, redirect, url_for, flash DB_PATH = os.environ.get("WEAKNESS_DB_PATH", "doordash.db") # CPI-U annual average multipliers to 2026 dollars CPI_TO_2026 = { "2019": 1.28, "2020": 1.25, "2021": 1.18, "2022": 1.10, "2023": 1.06, "2024": 1.03, "2025": 1.01, "2026": 1.00, } app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY", os.urandom(32)) app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16MB max upload EXPECTED_HEADER = [ "ITEM", "CATEGORY", "STORE_NAME", "UNIT_PRICE", "QUANTITY", "SUBTOTAL", "CREATED_AT", "DELIVERY_TIME", "DELIVERY_ADDRESS", ] def compute_wrapped(subset_rows, inflation_adjust=False): if not subset_rows: return None ss = defaultdict(float) hc = defaultdict(int) wc = defaultdict(int) ms = defaultdict(float) ic = defaultdict(int) orders = set() t_spent = 0.0 for r in subset_rows: sub = float(r["SUBTOTAL"]) if inflation_adjust: try: yr = str(to_est(r["CREATED_AT"]).year) except (ValueError, TypeError): yr = "2026" sub *= CPI_TO_2026.get(yr, 1.0) t_spent += sub ss[r["STORE_NAME"]] += sub ic[r["ITEM"]] += 1 try: dt = to_est(r["CREATED_AT"]) except (ValueError, TypeError): continue orders.add(r["CREATED_AT"]) hc[dt.hour] += 1 wc[dt.strftime("%A")] += 1 ms[dt.strftime("%Y-%m")] += sub fs = max(ss.items(), key=lambda x: x[1]) ph = max(hc.items(), key=lambda x: x[1]) pd = max(wc.items(), key=lambda x: x[1]) bm = max(ms.items(), key=lambda x: x[1]) fi = max(ic.items(), key=lambda x: x[1]) avg = round(t_spent / len(orders), 2) if orders else 0 return { "fav_store_name": fs[0], "fav_store_total": f"${fs[1]:,.2f}", "fav_store_pct": round(fs[1] / t_spent * 100, 1) if t_spent else 0, "peak_hour_label": f"{ph[0]}:00 EST", "peak_hour_count": ph[1], "peak_day_name": pd[0], "biggest_month_label": datetime.strptime(bm[0], "%Y-%m").strftime("%B %Y"), "biggest_month_total": f"${bm[1]:,.2f}", "avg_per_order": f"${avg:,.2f}", "fav_item_name": fi[0], "fav_item_count": fi[1], "total_spent": f"${t_spent:,.2f}", "total_items": len(subset_rows), "total_orders": len(orders), } def render_dashboard(rows): import numpy as np total_spent = sum(float(r["SUBTOTAL"]) for r in rows) total_orders_set = set() store_spend = defaultdict(float) month_spend = defaultdict(float) hour_counts = defaultdict(int) weekday_counts = defaultdict(int) week_grid = [[0] * 7 for _ in range(53)] for r in rows: store_spend[r["STORE_NAME"]] += float(r["SUBTOTAL"]) try: dt = to_est(r["CREATED_AT"]) except (ValueError, TypeError): continue total_orders_set.add(r["CREATED_AT"]) month_key = dt.strftime("%Y-%m") month_spend[month_key] += float(r["SUBTOTAL"]) hour_counts[dt.hour] += 1 weekday_counts[dt.strftime("%A")] += 1 week = min(dt.isocalendar().week - 1, 52) day = dt.isocalendar().weekday - 1 week_grid[week][day] += float(r["SUBTOTAL"]) year_data = defaultdict(list) for r in rows: try: dt = to_est(r["CREATED_AT"]) year_data[str(dt.year)].append(r) except (ValueError, TypeError): pass wrapped_by_year = {"All Time": compute_wrapped(rows)} wrapped_by_year_adj = {"All Time": compute_wrapped(rows, inflation_adjust=True)} for year in sorted(year_data.keys()): wrapped_by_year[year] = compute_wrapped(year_data[year]) wrapped_by_year_adj[year] = compute_wrapped(year_data[year], inflation_adjust=True) top_stores = sorted(store_spend.items(), key=lambda x: x[1], reverse=True)[:15] store_names = [s[0] for s in top_stores] store_totals = [round(s[1], 2) for s in top_stores] sorted_months = sorted(month_spend.keys()) month_totals = [round(month_spend[m], 2) for m in sorted_months] hour_labels = list(range(24)) hour_values = [hour_counts.get(h, 0) for h in hour_labels] hour_labels_str = [f"{h}:00 EST" for h in hour_labels] day_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] weekday_values = [weekday_counts.get(d, 0) for d in day_order] grid = np.array(week_grid) heatmap_data = np.rot90(grid).tolist() heatmap_days = ["Sun", "Sat", "Fri", "Thu", "Wed", "Tue", "Mon"] return render_template_string( TEMPLATE, total_spent=f"${total_spent:,.2f}", total_items=len(rows), total_orders=len(total_orders_set), store_names=json.dumps(store_names), store_totals=json.dumps(store_totals), month_labels=json.dumps(sorted_months), month_totals=json.dumps(month_totals), hour_labels=json.dumps(hour_labels_str), hour_values=json.dumps(hour_values), day_order=json.dumps(day_order), weekday_values=json.dumps(weekday_values), heatmap_data=json.dumps(heatmap_data), heatmap_days=json.dumps(heatmap_days), wrapped_data=json.dumps(wrapped_by_year), wrapped_data_adj=json.dumps(wrapped_by_year_adj), ) def parse_csv_to_rows(text): reader = csv.reader(io.StringIO(text)) header = [h.strip().upper() for h in next(reader)] if header != EXPECTED_HEADER: return None, "Invalid CSV header" rows = [] for row in reader: if len(row) != 9: continue rows.append({ "ITEM": row[0], "CATEGORY": row[1], "STORE_NAME": row[2], "UNIT_PRICE": row[3], "QUANTITY": row[4], "SUBTOTAL": row[5], "CREATED_AT": row[6], "DELIVERY_TIME": row[7], "DELIVERY_ADDRESS": row[8], }) return rows, None @app.route("/") def home(): return render_template_string(HOME_TEMPLATE) @app.route("/dashboard") def dashboard(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute( "SELECT ITEM, STORE_NAME, UNIT_PRICE, QUANTITY, SUBTOTAL, CREATED_AT FROM doordash" ).fetchall() conn.close() return render_dashboard(rows) @app.route("/upload", methods=["GET", "POST"]) def upload(): if request.method == "POST": file = request.files.get("csv_file") if not file or not file.filename.endswith(".csv"): flash("Please upload a .csv file", "error") return redirect(url_for("home")) text = file.stream.read().decode("utf-8") rows, err = parse_csv_to_rows(text) if err: flash(err, "error") return redirect(url_for("home")) return render_dashboard(rows) return redirect(url_for("home")) HOME_TEMPLATE = """
See exactly how much you've been spending on delivery
{% with messages = get_flashed_messages(with_categories=true) %} {% for cat, msg in messages %}Drop your DoorDash CSV export to see your dashboard. Nothing is stored.
DoorDash spending dashboard
{% with messages = get_flashed_messages(with_categories=true) %} {% for cat, msg in messages %}A look at your most questionable financial decisions
See how your spending correlates with your weight