generated from coulomb/repo-seed
Refactor economics to expense-record ledger with correct Bubble cost
Replace pre-aggregated costs.json with expense_records.json (48 line-item records) and payment_records.json. All monthly and cumulative totals are computed deterministically in observatory/ledger.py. Correct Bubble.io to $32/mo (since Feb 2025) — infrastructure €69.44/mo not €72.20.
This commit is contained in:
@@ -4,16 +4,15 @@ import json
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
_ZERO = Decimal("0")
|
||||
|
||||
from .ledger import build_monthly_ledger
|
||||
from .models import (
|
||||
Budget,
|
||||
CostEntry,
|
||||
ExpenseRecord,
|
||||
MembershipRecord,
|
||||
MonthlyPlatformCost,
|
||||
PaymentRecord,
|
||||
PricingModel,
|
||||
Product,
|
||||
RevenueEntry,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,55 +66,38 @@ def load_pricing_models(data_dir: Path | None = None) -> list[PricingModel]:
|
||||
]
|
||||
|
||||
|
||||
def _parse_cost_entries(items: list[dict]) -> list[CostEntry]:
|
||||
def load_fx_rates(data_dir: Path | None = None) -> dict[str, Decimal]:
|
||||
raw = _read_json((data_dir or default_data_dir()) / "expense_records.json")
|
||||
return {pair: _money(rate) for pair, rate in raw.get("fx_rates", {}).items()}
|
||||
|
||||
|
||||
def load_expense_records(data_dir: Path | None = None) -> list[ExpenseRecord]:
|
||||
raw = _read_json((data_dir or default_data_dir()) / "expense_records.json")
|
||||
return [
|
||||
CostEntry(
|
||||
ExpenseRecord(
|
||||
id=item["id"],
|
||||
name=item["name"],
|
||||
category=item["category"],
|
||||
period=item["period"],
|
||||
vendor=item["vendor"],
|
||||
description=item["description"],
|
||||
cost_class=item["cost_class"],
|
||||
amount=_money(item["amount"]),
|
||||
currency=item["currency"],
|
||||
cadence=item["cadence"],
|
||||
allocation=item["allocation"],
|
||||
source=item["source"],
|
||||
)
|
||||
for item in items
|
||||
for item in raw["records"]
|
||||
]
|
||||
|
||||
|
||||
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")
|
||||
rows: list[MonthlyPlatformCost] = []
|
||||
for item in raw.get("monthly_history", []):
|
||||
if "infrastructure_cost" in item:
|
||||
infrastructure = _money(item["infrastructure_cost"])
|
||||
processing = _money(item.get("payment_processing_cost", "0"))
|
||||
else:
|
||||
# Legacy single platform_cost field treated as infrastructure only.
|
||||
infrastructure = _money(item["platform_cost"])
|
||||
processing = _ZERO
|
||||
rows.append(
|
||||
MonthlyPlatformCost(
|
||||
period=item["period"],
|
||||
infrastructure_cost=infrastructure,
|
||||
payment_processing_cost=processing,
|
||||
active_members=item["active_members"],
|
||||
gross_revenue=_money(item["gross_revenue"]),
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def load_revenue(data_dir: Path | None = None) -> list[RevenueEntry]:
|
||||
raw = _read_json((data_dir or default_data_dir()) / "revenue.json")
|
||||
def load_payment_records(data_dir: Path | None = None) -> list[PaymentRecord]:
|
||||
root = data_dir or default_data_dir()
|
||||
path = root / "payment_records.json"
|
||||
if not path.exists():
|
||||
# Backward compatibility with legacy revenue.json
|
||||
path = root / "revenue.json"
|
||||
raw = _read_json(path)
|
||||
items = raw.get("records", raw.get("entries", []))
|
||||
return [
|
||||
RevenueEntry(
|
||||
PaymentRecord(
|
||||
id=item["id"],
|
||||
period=item["period"],
|
||||
gross_amount=_money(item["gross_amount"]),
|
||||
@@ -124,8 +106,9 @@ def load_revenue(data_dir: Path | None = None) -> list[RevenueEntry]:
|
||||
net_amount=_money(item["net_amount"]),
|
||||
currency=item["currency"],
|
||||
source=item["source"],
|
||||
member_count=item.get("member_count", 0),
|
||||
)
|
||||
for item in raw["entries"]
|
||||
for item in items
|
||||
]
|
||||
|
||||
|
||||
@@ -143,5 +126,16 @@ def load_membership(data_dir: Path | None = None) -> list[MembershipRecord]:
|
||||
]
|
||||
|
||||
|
||||
def load_monthly_ledger(data_dir: Path | None = None) -> list[MonthlyPlatformCost]:
|
||||
root = data_dir or default_data_dir()
|
||||
return build_monthly_ledger(
|
||||
load_budget(root),
|
||||
load_expense_records(root),
|
||||
load_payment_records(root),
|
||||
load_membership(root),
|
||||
load_fx_rates(root),
|
||||
)
|
||||
|
||||
|
||||
def latest_period(monthly_costs: list[MonthlyPlatformCost]) -> str:
|
||||
return max(item.period for item in monthly_costs)
|
||||
Reference in New Issue
Block a user