Add liquidity tracking, budget, and platform cost history

Restore operator platform costs (Bubble, domains, Stripe) with monthly
history from March 2025 and member payments from November 2025. Track
€1,000 starting budget, cumulative burn, and remaining liquidity in the
economics dashboard. Document LQ requirements in REQUIREMENTS.md.
This commit is contained in:
2026-06-22 01:48:45 +02:00
parent 8f42220d81
commit fe2174f37a
12 changed files with 497 additions and 112 deletions

View File

@@ -5,8 +5,10 @@ from decimal import Decimal
from pathlib import Path
from .models import (
Budget,
CostEntry,
MembershipRecord,
MonthlyPlatformCost,
PricingModel,
Product,
RevenueEntry,
@@ -37,6 +39,15 @@ def load_product(data_dir: Path | None = None) -> Product:
)
def load_budget(data_dir: Path | None = None) -> Budget:
raw = _read_json((data_dir or default_data_dir()) / "budget.json")
return Budget(
currency=raw["currency"],
initial_budget=_money(raw["initial_budget"]),
started=raw["started"],
)
def load_pricing_models(data_dir: Path | None = None) -> list[PricingModel]:
raw = _read_json((data_dir or default_data_dir()) / "pricing-models.json")
return [
@@ -54,9 +65,8 @@ def load_pricing_models(data_dir: Path | None = None) -> list[PricingModel]:
]
def load_costs(data_dir: Path | None = None) -> tuple[str, list[CostEntry], dict[str, Decimal]]:
raw = _read_json((data_dir or default_data_dir()) / "costs.json")
entries = [
def _parse_cost_entries(items: list[dict]) -> list[CostEntry]:
return [
CostEntry(
id=item["id"],
name=item["name"],
@@ -66,10 +76,28 @@ def load_costs(data_dir: Path | None = None) -> tuple[str, list[CostEntry], dict
cadence=item["cadence"],
allocation=item["allocation"],
)
for item in raw["entries"]
for item in items
]
def load_cost_rate_card(data_dir: Path | None = None) -> tuple[list[CostEntry], dict[str, Decimal]]:
raw = _read_json((data_dir or default_data_dir()) / "costs.json")
items = raw.get("rate_card", raw.get("entries", []))
fx = {pair: _money(rate) for pair, rate in raw.get("fx_rates", {}).items()}
return raw.get("period", ""), entries, fx
return _parse_cost_entries(items), fx
def load_monthly_platform_costs(data_dir: Path | None = None) -> list[MonthlyPlatformCost]:
raw = _read_json((data_dir or default_data_dir()) / "costs.json")
return [
MonthlyPlatformCost(
period=item["period"],
platform_cost=_money(item["platform_cost"]),
active_members=item["active_members"],
gross_revenue=_money(item["gross_revenue"]),
)
for item in raw.get("monthly_history", [])
]
def load_revenue(data_dir: Path | None = None) -> list[RevenueEntry]:
@@ -100,4 +128,8 @@ def load_membership(data_dir: Path | None = None) -> list[MembershipRecord]:
churned_at=item.get("churned_at"),
)
for item in raw["members"]
]
]
def latest_period(monthly_costs: list[MonthlyPlatformCost]) -> str:
return max(item.period for item in monthly_costs)