#!/bin/sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) export REPORT_DIR="$SCRIPT_DIR" python3 - <<'PY' import csv import json import os from collections import defaultdict from datetime import date, datetime, timedelta, timezone from pathlib import Path root = Path(os.environ["REPORT_DIR"]) package = "com.amazon.mShop.android.shopping" geo = "ZA" launch_start = date(2026, 8, 1) launch_end = date(2026, 8, 31) monthly_budget_zar = 511_500.0 daily_budget_zar = 16_500.0 unit_price_zar = 49.5 fx_zar_per_usd = 16.5 spend = defaultdict(float) with (root / "source-delivery.csv").open(encoding="utf-8-sig", newline="") as f: for row in csv.DictReader(f): if row.get("广告包") == package and row.get("国家") == geo: day = row.get("日期", "") if len(day) == 8 and day.isdigit(): spend[day] += float(row.get("平台消耗") or 0) events = defaultdict(lambda: defaultdict(int)) with (root / "source-events.csv").open(encoding="utf-8-sig", newline="") as f: reader = csv.reader(f) next(reader, None) for row in reader: if len(row) < 7 or row[2] != package or row[3] != geo: continue name, day = row[0], row[1] if name not in {"af_app_opened", "event1", "event2"}: continue # The source export leaves its last three headers blank. The second # numeric column after midas_campaign_id is the repostback count. try: repostback = int(float(row[6] or 0)) except (ValueError, TypeError): repostback = 0 key = "install" if name == "af_app_opened" else name events[day][key] += repostback source_days = sorted(set(spend) | set(events)) data_cutoff = source_days[-1] if source_days else None days = [] cursor = launch_start while cursor <= launch_end: days.append(cursor.strftime("%Y%m%d")) cursor += timedelta(days=1) daily = [] for day in days: install = events[day]["install"] event1 = events[day]["event1"] event2 = events[day]["event2"] conversions = event1 + event2 payable_zar = conversions * unit_price_zar midas_spend_usd = round(spend[day], 2) is_actual = bool(data_cutoff and day <= data_cutoff) daily.append({ "date": f"{day[:4]}-{day[4:6]}-{day[6:]}", "is_actual": is_actual, "install": install, "event1": event1, "event2": event2, "e1e2": conversions, "e1e2_rate": round(conversions / install * 100, 2) if install else None, "midas_spend": midas_spend_usd, "midas_spend_zar": round(midas_spend_usd * fx_zar_per_usd, 2), "payable_spend": round(conversions * 3, 2), "payable_spend_zar": round(payable_zar, 2), "daily_budget_zar": daily_budget_zar, "daily_cap": round(daily_budget_zar / unit_price_zar), "ffr_rate": round(conversions / (daily_budget_zar / unit_price_zar) * 100, 2), "ecpi_usd": round(midas_spend_usd / install, 4) if install else None, }) actual_daily = [x for x in daily if x["is_actual"]] totals = { "install": sum(x["install"] for x in actual_daily), "event1": sum(x["event1"] for x in actual_daily), "event2": sum(x["event2"] for x in actual_daily), "e1e2": sum(x["e1e2"] for x in actual_daily), "midas_spend": round(sum(x["midas_spend"] for x in actual_daily), 2), "midas_spend_zar": round(sum(x["midas_spend_zar"] for x in actual_daily), 2), "payable_spend": round(sum(x["payable_spend"] for x in actual_daily), 2), "payable_spend_zar": round(sum(x["payable_spend_zar"] for x in actual_daily), 2), } totals["e1e2_rate"] = round(totals["e1e2"] / totals["install"] * 100, 2) if totals["install"] else None elapsed_days = len(actual_daily) remaining_days = max((launch_end - datetime.strptime(data_cutoff, "%Y%m%d").date()).days, 0) if data_cutoff else (launch_end - launch_start).days + 1 expected_budget_to_date = min(elapsed_days * daily_budget_zar, monthly_budget_zar) remaining_budget_zar = monthly_budget_zar - totals["payable_spend_zar"] avg_daily_used = totals["payable_spend_zar"] / elapsed_days if elapsed_days else 0 projected_month_spend = avg_daily_used * ((launch_end - launch_start).days + 1) budget = { "monthly_budget_zar": monthly_budget_zar, "monthly_budget_usd": round(monthly_budget_zar / fx_zar_per_usd, 2), "daily_budget_zar": daily_budget_zar, "daily_budget_usd": round(daily_budget_zar / fx_zar_per_usd, 2), "unit_price_zar": unit_price_zar, "unit_price_usd": round(unit_price_zar / fx_zar_per_usd, 2), "fx_zar_per_usd": fx_zar_per_usd, "daily_cap": round(daily_budget_zar / unit_price_zar), "used_budget_zar": totals["payable_spend_zar"], "used_rate": round(totals["payable_spend_zar"] / monthly_budget_zar * 100, 2), "remaining_budget_zar": round(remaining_budget_zar, 2), "remaining_budget_usd": round(remaining_budget_zar / fx_zar_per_usd, 2), "expected_budget_to_date_zar": expected_budget_to_date, "pacing_rate": round(totals["payable_spend_zar"] / expected_budget_to_date * 100, 2) if expected_budget_to_date else None, "elapsed_days": elapsed_days, "remaining_days": remaining_days, "required_daily_budget_zar": round(remaining_budget_zar / remaining_days, 2) if remaining_days else 0, "required_daily_events": round(remaining_budget_zar / remaining_days / unit_price_zar) if remaining_days else 0, "projected_month_spend_zar": round(projected_month_spend, 2), "projected_month_variance_zar": round(monthly_budget_zar - projected_month_spend, 2), } payload = { "meta": { "package": package, "geo": geo, "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "date_start": daily[0]["date"] if daily else None, "date_end": daily[-1]["date"] if daily else None, "data_cutoff": f"{data_cutoff[:4]}-{data_cutoff[4:6]}-{data_cutoff[6:]}" if data_cutoff else None, "currency": "USD", "install_definition": "af_app_opened repostback", "event_definition": "event1/event2 repostback", "rate_definition": "(event1 + event2) / Install", "payable_definition": "(event1 + event2) × $3", }, "totals": totals, "budget": budget, "daily": daily, } tmp = root / "data.json.tmp" tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") tmp.replace(root / "data.json") PY