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:
2026-06-22 02:03:22 +02:00
parent ea2c2c6403
commit 31db9f8f31
12 changed files with 843 additions and 299 deletions

View File

@@ -9,26 +9,27 @@ from .load import (
default_data_dir,
latest_period,
load_budget,
load_expense_records,
load_membership,
load_monthly_platform_costs,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
load_revenue,
)
from .models import EconomicsSnapshot, LiquiditySummary, MonthlyPlatformCost, PricingModel, Product, RevenueEntry
from .models import EconomicsSnapshot, LiquiditySummary, MonthlyPlatformCost, PaymentRecord, PricingModel, Product
def _history_rows(
monthly_costs: list[MonthlyPlatformCost],
revenue_entries: list[RevenueEntry],
payments: list[PaymentRecord],
through_period: str,
) -> str:
revenue_by_period = {entry.period: entry for entry in revenue_entries}
payment_by_period = {record.period: record for record in payments}
lines: list[str] = []
for month in sorted(monthly_costs, key=lambda item: item.period):
if month.period > through_period:
continue
payment = revenue_by_period.get(month.period)
payment = payment_by_period.get(month.period)
net_payment = payment.net_amount if payment else Decimal("0")
period_net = net_payment - month.infrastructure_cost
lines.append(
@@ -45,14 +46,15 @@ def render_dashboard(
snapshot: EconomicsSnapshot,
liquidity: LiquiditySummary,
monthly_costs: list[MonthlyPlatformCost],
revenue_entries: list[RevenueEntry],
payments: list[PaymentRecord],
expense_count: int,
) -> str:
active = next(m for m in models if m.id == product.active_pricing_model_id)
registry_lines = "\n".join(
f"| {model.id} | {model.name} | {model.model_type} | {model.status} |"
for model in models
)
history_lines = _history_rows(monthly_costs, revenue_entries, snapshot.period)
history_lines = _history_rows(monthly_costs, payments, snapshot.period)
budget_state = (
"over budget"
if liquidity.remaining_budget < Decimal("0")
@@ -66,7 +68,9 @@ def render_dashboard(
**Active pricing model:** {active.name} ({active.access_fee_amount} {active.currency}/{active.access_fee_cadence})
> Platform costs accrue to the operator. Customer cost-pass-through billing is
> **not active** in MVP — members pay subscription only.
> **not active** in MVP — members pay subscription only. All totals are computed
> programmatically from expense and payment record ledgers ({expense_count} expense
> records).
## Key Metrics (current period)
@@ -114,9 +118,8 @@ _Revenue source: {snapshot.revenue_source}_
- Product model (`data/product.json`)
- Budget (`data/budget.json`)
- Pricing model registry (`data/pricing-models.json`)
- Platform costs (`data/costs.json`)
- Member payments (`data/revenue.json`)
- Expense records (`data/expense_records.json`) — source of truth for costs
- Payment records (`data/payment_records.json`)
- Membership (`data/membership.json`)
- Requirements (`REQUIREMENTS.md`)
"""
@@ -128,13 +131,16 @@ def generate_dashboard(data_dir: Path | None = None, period: str | None = None)
budget = load_budget(root)
models = load_pricing_models(root)
members = load_membership(root)
revenue = load_revenue(root)
monthly_costs = load_monthly_platform_costs(root)
payments = load_payment_records(root)
expenses = load_expense_records(root)
monthly_costs = load_monthly_ledger(root)
target_period = period or latest_period(monthly_costs)
snapshot = build_snapshot(target_period, product, models, members, revenue, monthly_costs)
liquidity = build_liquidity_summary(budget, revenue, monthly_costs, target_period)
return render_dashboard(product, models, snapshot, liquidity, monthly_costs, revenue)
snapshot = build_snapshot(target_period, product, models, members, payments, monthly_costs)
liquidity = build_liquidity_summary(budget, payments, monthly_costs, target_period)
return render_dashboard(
product, models, snapshot, liquidity, monthly_costs, payments, len(expenses)
)
def main(argv: list[str] | None = None) -> int: