generated from coulomb/repo-seed
Complete Economic Observatory MVP (ADAPTIVE-WP-0002)
Add file-based Bubble, Stripe, and OpenRouter importers; usage attribution, cost allocation, pricing simulator, credit wallets, and recommendations in the dashboard API. Document whynot-design UI workflow and archive the finished workplan with all ten tasks marked done.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""File-based importers for Bubble, Stripe, and OpenRouter exports."""
|
||||
13
projects/coulomb-pricing/observatory/importers/_io.py
Normal file
13
projects/coulomb-pricing/observatory/importers/_io.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_export(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_registry(path: Path, payload: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
55
projects/coulomb-pricing/observatory/importers/bubble.py
Normal file
55
projects/coulomb-pricing/observatory/importers/bubble.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from . import _io
|
||||
|
||||
BUBBLE_STATUS = {"Active": "active", "Cancelled": "churned", "Paused": "paused"}
|
||||
|
||||
|
||||
def import_membership(export: dict, plan_id: str = "flat-899-eur-monthly") -> dict:
|
||||
members = []
|
||||
for index, user in enumerate(export.get("users", []), start=1):
|
||||
bubble_id = user.get("bubble_id") or user.get("_id") or f"bubble-{index}"
|
||||
username = user.get("username") or user.get("email") or bubble_id
|
||||
status = BUBBLE_STATUS.get(user.get("status", "Active"), "active")
|
||||
members.append(
|
||||
{
|
||||
"id": f"member-{username}",
|
||||
"username": username,
|
||||
"external_id": bubble_id,
|
||||
"status": status,
|
||||
"joined_at": user.get("created") or user.get("joined_at"),
|
||||
"plan_id": user.get("plan") or plan_id,
|
||||
"source": "bubble",
|
||||
"churned_at": user.get("cancelled_at") if status == "churned" else None,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"version": 1,
|
||||
"snapshot_date": export.get("exported_at", export.get("snapshot_date")),
|
||||
"members": members,
|
||||
"note": "Imported from Bubble export",
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Import Bubble membership export")
|
||||
parser.add_argument("--input", type=Path, required=True, help="Bubble JSON export")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent.parent.parent / "data" / "membership.json",
|
||||
)
|
||||
parser.add_argument("--plan-id", default="flat-899-eur-monthly")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
payload = import_membership(_io.read_export(args.input), args.plan_id)
|
||||
_io.write_registry(args.output, payload)
|
||||
print(f"Wrote {len(payload['members'])} members → {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
57
projects/coulomb-pricing/observatory/importers/openrouter.py
Normal file
57
projects/coulomb-pricing/observatory/importers/openrouter.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from . import _io
|
||||
|
||||
|
||||
def _money(value: str | int | float) -> str:
|
||||
return f"{Decimal(str(value)):.2f}"
|
||||
|
||||
|
||||
def import_usage(export: dict, fx_usd_eur: str = "0.92") -> dict:
|
||||
rate = Decimal(str(fx_usd_eur))
|
||||
records = []
|
||||
for index, row in enumerate(export.get("usage", export.get("records", [])), start=1):
|
||||
cost_usd = Decimal(str(row.get("cost_usd") or row.get("cost", "0")))
|
||||
cost_eur = (cost_usd * rate).quantize(Decimal("0.01"))
|
||||
records.append(
|
||||
{
|
||||
"id": row.get("id") or f"usage-{row['period']}-{index}",
|
||||
"period": row["period"],
|
||||
"member_id": row.get("member_id") or row.get("user_id"),
|
||||
"model": row["model"],
|
||||
"tokens": row.get("tokens", 0),
|
||||
"cost_usd": _money(cost_usd),
|
||||
"cost_eur": _money(cost_eur),
|
||||
"source": "openrouter",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"version": 1,
|
||||
"fx_usd_eur": str(rate),
|
||||
"records": sorted(records, key=lambda item: (item["period"], item["id"])),
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Import OpenRouter usage export")
|
||||
parser.add_argument("--input", type=Path, required=True, help="OpenRouter JSON export")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent.parent.parent / "data" / "usage_records.json",
|
||||
)
|
||||
parser.add_argument("--fx-usd-eur", default="0.92")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
payload = import_usage(_io.read_export(args.input), args.fx_usd_eur)
|
||||
_io.write_registry(args.output, payload)
|
||||
print(f"Wrote {len(payload['records'])} usage records → {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
54
projects/coulomb-pricing/observatory/importers/stripe.py
Normal file
54
projects/coulomb-pricing/observatory/importers/stripe.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from . import _io
|
||||
|
||||
|
||||
def _money(value: str | int | float) -> str:
|
||||
return f"{Decimal(str(value)):.2f}"
|
||||
|
||||
|
||||
def import_payments(export: dict) -> dict:
|
||||
records = []
|
||||
for index, charge in enumerate(export.get("charges", export.get("records", [])), start=1):
|
||||
period = charge["period"]
|
||||
records.append(
|
||||
{
|
||||
"id": charge.get("id") or f"pay-{period}-{index}",
|
||||
"period": period,
|
||||
"gross_amount": _money(charge.get("gross") or charge["gross_amount"]),
|
||||
"fees_amount": _money(charge.get("fee") or charge.get("fees_amount", "0")),
|
||||
"refunds_amount": _money(charge.get("refunds_amount", "0")),
|
||||
"net_amount": _money(charge.get("net") or charge["net_amount"]),
|
||||
"currency": charge.get("currency", "EUR"),
|
||||
"source": "stripe",
|
||||
"member_count": charge.get("member_count", 1),
|
||||
"member_username": charge.get("customer") or charge.get("member_username"),
|
||||
"product": charge.get("product"),
|
||||
"payout_account": charge.get("payout_account"),
|
||||
}
|
||||
)
|
||||
return {"version": 2, "records": sorted(records, key=lambda item: item["period"])}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Import Stripe charge export")
|
||||
parser.add_argument("--input", type=Path, required=True, help="Stripe JSON export")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent.parent.parent / "data" / "payment_records.json",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
payload = import_payments(_io.read_export(args.input))
|
||||
_io.write_registry(args.output, payload)
|
||||
print(f"Wrote {len(payload['records'])} payment records → {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user