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.
135 lines
No EOL
4.1 KiB
Python
135 lines
No EOL
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
from .models import (
|
|
Budget,
|
|
CostEntry,
|
|
MembershipRecord,
|
|
MonthlyPlatformCost,
|
|
PricingModel,
|
|
Product,
|
|
RevenueEntry,
|
|
)
|
|
|
|
|
|
def _money(value: str | int | float) -> Decimal:
|
|
return Decimal(str(value))
|
|
|
|
|
|
def _read_json(path: Path) -> dict:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def default_data_dir() -> Path:
|
|
return Path(__file__).resolve().parent.parent / "data"
|
|
|
|
|
|
def load_product(data_dir: Path | None = None) -> Product:
|
|
raw = _read_json((data_dir or default_data_dir()) / "product.json")
|
|
return Product(
|
|
id=raw["id"],
|
|
name=raw["name"],
|
|
lifecycle_phase=raw["lifecycle_phase"],
|
|
currency=raw["currency"],
|
|
description=raw["description"],
|
|
active_pricing_model_id=raw["active_pricing_model_id"],
|
|
)
|
|
|
|
|
|
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 [
|
|
PricingModel(
|
|
id=item["id"],
|
|
name=item["name"],
|
|
model_type=item["model_type"],
|
|
lifecycle_phase=item["lifecycle_phase"],
|
|
currency=item["currency"],
|
|
access_fee_amount=_money(item["access_fee_amount"]),
|
|
access_fee_cadence=item["access_fee_cadence"],
|
|
status=item["status"],
|
|
)
|
|
for item in raw["models"]
|
|
]
|
|
|
|
|
|
def _parse_cost_entries(items: list[dict]) -> list[CostEntry]:
|
|
return [
|
|
CostEntry(
|
|
id=item["id"],
|
|
name=item["name"],
|
|
category=item["category"],
|
|
amount=_money(item["amount"]),
|
|
currency=item["currency"],
|
|
cadence=item["cadence"],
|
|
allocation=item["allocation"],
|
|
)
|
|
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 _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]:
|
|
raw = _read_json((data_dir or default_data_dir()) / "revenue.json")
|
|
return [
|
|
RevenueEntry(
|
|
id=item["id"],
|
|
period=item["period"],
|
|
gross_amount=_money(item["gross_amount"]),
|
|
fees_amount=_money(item["fees_amount"]),
|
|
refunds_amount=_money(item.get("refunds_amount", "0")),
|
|
net_amount=_money(item["net_amount"]),
|
|
currency=item["currency"],
|
|
source=item["source"],
|
|
)
|
|
for item in raw["entries"]
|
|
]
|
|
|
|
|
|
def load_membership(data_dir: Path | None = None) -> list[MembershipRecord]:
|
|
raw = _read_json((data_dir or default_data_dir()) / "membership.json")
|
|
return [
|
|
MembershipRecord(
|
|
id=item["id"],
|
|
status=item["status"],
|
|
joined_at=item["joined_at"],
|
|
plan_id=item["plan_id"],
|
|
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) |