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

@@ -6,19 +6,26 @@ Generic adaptive-pricing framework concepts belong in the repository root
(`INTENT.md`, `docs/`, `research/`, `registry/`). Execution tracking:
`workplans/ADAPTIVE-WP-0002-economic-observatory-mvp.md`.
Liquidity and cost requirements: `REQUIREMENTS.md`.
## Sprint 1 — Economic Foundations
The `observatory/` package loads JSON registries and produces **Economics
Dashboard v1** metrics:
Dashboard v1** with platform-cost history, member payments, and budget burn.
| Registry | File |
|----------|------|
| Budget (€1,000 start) | `data/budget.json` |
| Product model | `data/product.json` |
| Pricing models | `data/pricing-models.json` |
| Costs | `data/costs.json` |
| Revenue | `data/revenue.json` |
| Platform costs + history | `data/costs.json` |
| Member payments | `data/revenue.json` |
| Membership | `data/membership.json` |
**Current reality:** platform costs from March 2025; sole member payments from
November 2025. Operator is **burning liquidity** — platform spend exceeds
subscription intake. Customer cost-pass-through billing is not active.
### Commands
```bash
@@ -28,6 +35,5 @@ python3 -m observatory --period 2026-06
python3 -m observatory --period 2026-06 --output reports/economics-2026-06.md
```
**Current registry state:** one active member (founder), €8.99/month revenue, no
running costs recorded. Bubble (Sprint 2) and Stripe (Sprint 3) importers will
replace manual entries when integrations land.
Manual registries will be replaced by Bubble (Sprint 2), Stripe (Sprint 3), and
OpenRouter (Sprint 4) importers.

View File

@@ -0,0 +1,66 @@
# Coulomb MVP — Liquidity & Cost Requirements
## Context
Coulomb Social carries **operator platform costs** (Bubble.io, domains, Stripe
fees, future OpenRouter usage) while **customer cost-pass-through billing is not
active yet**. Members pay a flat subscription; they are not billed for
underlying platform spend.
The Economic Observatory must make the resulting **liquidity burn** visible.
## Requirements
### LQ-001 — Platform cost ledger
Record monthly platform costs from project start (March 2025), including fixed
infrastructure and payment-provider variable fees when member revenue exists.
### LQ-002 — Member payment ledger
Record member subscription payments from first payment month (November 2025),
separate from platform cost accrual.
### LQ-003 — Budget tracking
Maintain an operator liquidity budget (initial: **€1,000**) and compute
remaining budget after cumulative platform spend minus cumulative member
payments received.
### LQ-004 — Liquidity position
Report whether the project is **burning**, **neutral**, or **generating**
liquidity each period:
- `period_net = member_payments - platform_costs`
- `cumulative_net = sum(period_net)`
- `remaining_budget = initial_budget + cumulative_net`
Negative remaining budget means the MVP has consumed more liquidity than the
allocated budget.
### LQ-005 — No customer cost billing (MVP boundary)
Do not allocate platform costs to customer invoices in MVP. Cost attribution
(OpenRouter per member, usage overage) is observatory-only until a later phase
introduces customer-visible credits or usage billing.
### LQ-006 — Historical dashboard
Economics Dashboard must show:
- Current-period economics (revenue, platform cost, margin)
- Cumulative liquidity summary (budget, burn, remaining)
- Monthly history table from March 2025
## Data sources (current)
| Registry | Path |
|----------|------|
| Budget | `data/budget.json` |
| Platform costs | `data/costs.json` (`rate_card` + `monthly_history`) |
| Member payments | `data/revenue.json` |
| Membership | `data/membership.json` |
Future sprints replace manual history with Bubble, Stripe, and OpenRouter imports
while preserving the same liquidity semantics.

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"currency": "EUR",
"initial_budget": "1000.00",
"started": "2025-03",
"note": "Operator liquidity pool for Coulomb Social MVP platform spend before the offering is cash-flow positive."
}

View File

@@ -1,9 +1,72 @@
{
"version": 1,
"period": "2026-06",
"entries": [],
"version": 2,
"fx_rates": {
"USD/EUR": "0.92"
},
"note": "No running costs at present. Future fixed/variable lines (Bubble, Stripe, OpenRouter) added as they become payable."
"rate_card": [
{
"id": "bubble-subscription",
"name": "Bubble.io platform",
"category": "fixed",
"amount": "35.00",
"currency": "USD",
"cadence": "monthly",
"allocation": "flat"
},
{
"id": "domains",
"name": "Domains",
"category": "fixed",
"amount": "15.00",
"currency": "EUR",
"cadence": "monthly",
"allocation": "flat"
},
{
"id": "operational-overhead",
"name": "Operational overhead",
"category": "fixed",
"amount": "25.00",
"currency": "EUR",
"cadence": "monthly",
"allocation": "flat"
},
{
"id": "stripe-percentage",
"name": "Stripe percentage fee",
"category": "variable",
"amount": "0.015",
"currency": "ratio",
"cadence": "per_transaction",
"allocation": "percent_of_gross_revenue"
},
{
"id": "stripe-fixed",
"name": "Stripe fixed fee",
"category": "variable",
"amount": "0.25",
"currency": "EUR",
"cadence": "per_transaction",
"allocation": "per_active_member"
}
],
"monthly_history": [
{"period": "2025-03", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-04", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-05", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-06", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-07", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-08", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-09", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-10", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-11", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2025-12", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-01", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-02", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-03", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-04", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-05", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2026-06", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"}
],
"note": "Platform costs are operator expenses. Customer cost-pass-through billing is not active yet."
}

View File

@@ -9,8 +9,8 @@
"joined_at": "2025-11-03",
"plan_id": "flat-899-eur-monthly",
"source": "manual",
"note": "Sole active member (founder)"
"note": "Sole paying member; payments from November 2025"
}
],
"note": "Current reality: one active member. Bubble importer (Sprint 2) will sync live data."
"note": "Platform costs run from March 2025; member payments from November 2025."
}

View File

@@ -1,16 +1,14 @@
{
"version": 1,
"entries": [
{
"id": "rev-2026-06-founder",
"period": "2026-06",
"gross_amount": "8.99",
"fees_amount": "0.00",
"refunds_amount": "0.00",
"net_amount": "8.99",
"currency": "EUR",
"source": "manual",
"note": "Single-member subscription; Stripe sync in Sprint 3"
}
]
{"id": "rev-2025-11", "period": "2025-11", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2025-12", "period": "2025-12", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-01", "period": "2026-01", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-02", "period": "2026-02", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-03", "period": "2026-03", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-04", "period": "2026-04", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-05", "period": "2026-05", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1},
{"id": "rev-2026-06", "period": "2026-06", "gross_amount": "8.99", "fees_amount": "0.38", "refunds_amount": "0.00", "net_amount": "8.61", "currency": "EUR", "source": "manual", "member_count": 1}
],
"note": "Member payments begin November 2025. Stripe sync replaces manual entries in Sprint 3."
}

View File

@@ -1,49 +1,103 @@
from __future__ import annotations
import argparse
from decimal import Decimal
from pathlib import Path
from .economics import build_snapshot
from .economics import build_liquidity_summary, build_snapshot
from .load import (
default_data_dir,
load_costs,
latest_period,
load_budget,
load_membership,
load_monthly_platform_costs,
load_pricing_models,
load_product,
load_revenue,
)
from .models import EconomicsSnapshot, PricingModel, Product
from .models import EconomicsSnapshot, LiquiditySummary, MonthlyPlatformCost, PricingModel, Product, RevenueEntry
def _history_rows(
monthly_costs: list[MonthlyPlatformCost],
revenue_entries: list[RevenueEntry],
through_period: str,
) -> str:
revenue_by_period = {entry.period: entry for entry in revenue_entries}
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)
net_payment = payment.net_amount if payment else Decimal("0")
period_net = net_payment - month.platform_cost
lines.append(
f"| {month.period} | {month.active_members} | {month.gross_revenue} | "
f"{month.platform_cost} | {period_net:.2f} |"
)
return "\n".join(lines)
def render_dashboard(
product: Product,
models: list[PricingModel],
snapshot: EconomicsSnapshot,
liquidity: LiquiditySummary,
monthly_costs: list[MonthlyPlatformCost],
revenue_entries: list[RevenueEntry],
) -> 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)
budget_state = (
"over budget"
if liquidity.remaining_budget < Decimal("0")
else "within budget"
)
return f"""# Economics Dashboard v1 — {product.name}
**Period:** {snapshot.period}
**Lifecycle phase:** {product.lifecycle_phase}
**Active pricing model:** {active.name} ({active.access_fee_amount} {active.currency}/{active.access_fee_cadence})
## Key Metrics
> Platform costs accrue to the operator. Customer cost-pass-through billing is
> **not active** in MVP — members pay subscription only.
## Key Metrics (current period)
| Metric | Value |
|--------|------:|
| Active members | {snapshot.active_members} |
| Monthly revenue | {snapshot.monthly_revenue} {snapshot.currency} |
| Monthly cost | {snapshot.monthly_cost} {snapshot.currency} |
| Cost per member | {snapshot.cost_per_member} {snapshot.currency} |
| Gross margin | {snapshot.gross_margin} {snapshot.currency} |
| Gross margin % | {snapshot.gross_margin_pct}% |
| Member payments (gross) | {snapshot.monthly_revenue} {snapshot.currency} |
| Platform cost | {snapshot.monthly_platform_cost} {snapshot.currency} |
| Platform cost per member | {snapshot.cost_per_member} {snapshot.currency} |
| Period gross margin | {snapshot.gross_margin} {snapshot.currency} |
| Period gross margin % | {snapshot.gross_margin_pct}% |
| Period net liquidity | {snapshot.period_net_liquidity} {snapshot.currency} ({snapshot.liquidity_status}) |
_Revenue source: {snapshot.revenue_source}_
## Liquidity & Budget (through {liquidity.through_period})
| Metric | Value |
|--------|------:|
| Initial budget | {liquidity.initial_budget} {liquidity.currency} |
| Cumulative member payments (net) | {liquidity.cumulative_member_payments} {liquidity.currency} |
| Cumulative platform cost | {liquidity.cumulative_platform_cost} {liquidity.currency} |
| Cumulative net liquidity | {liquidity.cumulative_net_liquidity} {liquidity.currency} ({liquidity.liquidity_status}) |
| Remaining budget | {liquidity.remaining_budget} {liquidity.currency} ({budget_state}) |
| Months tracked | {liquidity.months_tracked} |
## Monthly History
| Period | Members | Gross revenue | Platform cost | Net liquidity |
|--------|--------:|--------------:|--------------:|--------------:|
{history_lines}
## Pricing Model Registry
| ID | Name | Type | Status |
@@ -53,32 +107,28 @@ _Revenue source: {snapshot.revenue_source}_
## Registries Loaded
- Product model (`data/product.json`)
- Budget (`data/budget.json`)
- Pricing model registry (`data/pricing-models.json`)
- Cost registry (`data/costs.json`)
- Revenue registry (`data/revenue.json`)
- Membership registry (`data/membership.json`)
- Platform costs (`data/costs.json`)
- Member payments (`data/revenue.json`)
- Membership (`data/membership.json`)
- Requirements (`REQUIREMENTS.md`)
"""
def generate_dashboard(data_dir: Path | None = None, period: str | None = None) -> str:
root = data_dir or default_data_dir()
product = load_product(root)
budget = load_budget(root)
models = load_pricing_models(root)
members = load_membership(root)
revenue = load_revenue(root)
cost_period, costs, fx_rates = load_costs(root)
target_period = period or cost_period
monthly_costs = load_monthly_platform_costs(root)
target_period = period or latest_period(monthly_costs)
snapshot = build_snapshot(
target_period,
product,
models,
members,
revenue,
costs,
fx_rates,
)
return render_dashboard(product, models, snapshot)
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)
def main(argv: list[str] | None = None) -> int:

View File

@@ -3,9 +3,13 @@ from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from .models import (
Budget,
CostEntry,
EconomicsSnapshot,
LiquidityStatus,
LiquiditySummary,
MembershipRecord,
MonthlyPlatformCost,
PricingModel,
Product,
RevenueEntry,
@@ -29,6 +33,14 @@ def _convert_to_eur(amount: Decimal, currency: str, fx_rates: dict[str, Decimal]
raise ValueError(f"unsupported currency for conversion: {currency}")
def liquidity_status_for(net: Decimal) -> LiquidityStatus:
if net < Decimal("0"):
return "burning"
if net > Decimal("0"):
return "generating"
return "neutral"
def active_members(members: list[MembershipRecord]) -> int:
return sum(1 for member in members if member.status == "active")
@@ -40,23 +52,7 @@ def active_pricing_model(models: list[PricingModel], product: Product) -> Pricin
raise ValueError(f"active pricing model not found: {product.active_pricing_model_id}")
def estimate_monthly_revenue(
period: str,
product: Product,
models: list[PricingModel],
members: list[MembershipRecord],
revenue_entries: list[RevenueEntry],
) -> tuple[Decimal, str]:
for entry in revenue_entries:
if entry.period == period and entry.currency == product.currency:
return entry.gross_amount, entry.source
model = active_pricing_model(models, product)
count = active_members(members)
return model.access_fee_amount * count, "derived_from_membership"
def monthly_cost_total(
def monthly_cost_from_rate_card(
cost_entries: list[CostEntry],
fx_rates: dict[str, Decimal],
gross_revenue: Decimal,
@@ -73,37 +69,106 @@ def monthly_cost_total(
return _quantize(total)
def revenue_for_period(period: str, revenue_entries: list[RevenueEntry]) -> RevenueEntry | None:
for entry in revenue_entries:
if entry.period == period:
return entry
return None
def estimate_monthly_revenue(
period: str,
product: Product,
models: list[PricingModel],
members: list[MembershipRecord],
revenue_entries: list[RevenueEntry],
monthly_costs: list[MonthlyPlatformCost],
) -> tuple[Decimal, Decimal, str]:
recorded = revenue_for_period(period, revenue_entries)
if recorded:
return recorded.gross_amount, recorded.net_amount, recorded.source
month = next((item for item in monthly_costs if item.period == period), None)
if month and month.gross_revenue > 0:
return month.gross_revenue, month.gross_revenue, "derived_from_platform_history"
model = active_pricing_model(models, product)
count = active_members(members)
gross = model.access_fee_amount * count
return gross, gross, "derived_from_membership"
def periods_through(target: str, monthly_costs: list[MonthlyPlatformCost]) -> list[str]:
return sorted(item.period for item in monthly_costs if item.period <= target)
def build_liquidity_summary(
budget: Budget,
revenue_entries: list[RevenueEntry],
monthly_costs: list[MonthlyPlatformCost],
through_period: str,
) -> LiquiditySummary:
tracked = periods_through(through_period, monthly_costs)
cost_by_period = {item.period: item for item in monthly_costs}
cumulative_payments = Decimal("0")
cumulative_cost = Decimal("0")
for period in tracked:
month = cost_by_period[period]
cumulative_cost += month.platform_cost
payment = revenue_for_period(period, revenue_entries)
cumulative_payments += payment.net_amount if payment else Decimal("0")
cumulative_net = _quantize(cumulative_payments - cumulative_cost)
remaining = _quantize(budget.initial_budget + cumulative_net)
return LiquiditySummary(
currency=budget.currency,
through_period=through_period,
initial_budget=budget.initial_budget,
cumulative_member_payments=_quantize(cumulative_payments),
cumulative_platform_cost=_quantize(cumulative_cost),
cumulative_net_liquidity=cumulative_net,
remaining_budget=remaining,
liquidity_status=liquidity_status_for(cumulative_net),
months_tracked=len(tracked),
)
def build_snapshot(
period: str,
product: Product,
models: list[PricingModel],
members: list[MembershipRecord],
revenue_entries: list[RevenueEntry],
cost_entries: list[CostEntry],
fx_rates: dict[str, Decimal],
monthly_costs: list[MonthlyPlatformCost],
) -> EconomicsSnapshot:
count = active_members(members)
gross_revenue, revenue_source = estimate_monthly_revenue(
period, product, models, members, revenue_entries
month = next(item for item in monthly_costs if item.period == period)
count = month.active_members if month.active_members else active_members(members)
gross_revenue, net_revenue, revenue_source = estimate_monthly_revenue(
period, product, models, members, revenue_entries, monthly_costs
)
monthly_cost = monthly_cost_total(cost_entries, fx_rates, gross_revenue, count)
cost_per_member = _quantize(monthly_cost / count) if count else Decimal("0.00")
gross_margin = _quantize(gross_revenue - monthly_cost)
platform_cost = month.platform_cost
cost_per_member = _quantize(platform_cost / count) if count else Decimal("0.00")
gross_margin = _quantize(gross_revenue - platform_cost)
margin_pct = (
_quantize((gross_margin / gross_revenue) * Decimal("100"), PCTPLACES)
if gross_revenue
else Decimal("0.0")
else Decimal("-100.0") if platform_cost else Decimal("0.0")
)
period_net = _quantize(net_revenue - platform_cost)
return EconomicsSnapshot(
period=period,
currency=product.currency,
active_members=count,
monthly_revenue=_quantize(gross_revenue),
monthly_cost=monthly_cost,
monthly_platform_cost=platform_cost,
cost_per_member=cost_per_member,
gross_margin=gross_margin,
gross_margin_pct=margin_pct,
pricing_model_count=len(models),
revenue_source=revenue_source,
period_net_liquidity=period_net,
liquidity_status=liquidity_status_for(period_net),
)

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)

View File

@@ -7,6 +7,7 @@ from typing import Literal
CostCategory = Literal["fixed", "variable"]
MemberStatus = Literal["active", "churned", "paused"]
PricingModelStatus = Literal["active", "candidate", "retired"]
LiquidityStatus = Literal["burning", "neutral", "generating"]
@dataclass(frozen=True)
@@ -42,6 +43,21 @@ class CostEntry:
allocation: str
@dataclass(frozen=True)
class MonthlyPlatformCost:
period: str
platform_cost: Decimal
active_members: int
gross_revenue: Decimal
@dataclass(frozen=True)
class Budget:
currency: str
initial_budget: Decimal
started: str
@dataclass(frozen=True)
class RevenueEntry:
id: str
@@ -69,9 +85,24 @@ class EconomicsSnapshot:
currency: str
active_members: int
monthly_revenue: Decimal
monthly_cost: Decimal
monthly_platform_cost: Decimal
cost_per_member: Decimal
gross_margin: Decimal
gross_margin_pct: Decimal
pricing_model_count: int
revenue_source: str
revenue_source: str
period_net_liquidity: Decimal
liquidity_status: LiquidityStatus
@dataclass(frozen=True)
class LiquiditySummary:
currency: str
through_period: str
initial_budget: Decimal
cumulative_member_payments: Decimal
cumulative_platform_cost: Decimal
cumulative_net_liquidity: Decimal
remaining_budget: Decimal
liquidity_status: LiquidityStatus
months_tracked: int

View File

@@ -4,19 +4,55 @@
**Lifecycle phase:** growth
**Active pricing model:** Standard Membership (8.99 EUR/monthly)
## Key Metrics
> Platform costs accrue to the operator. Customer cost-pass-through billing is
> **not active** in MVP — members pay subscription only.
## Key Metrics (current period)
| Metric | Value |
|--------|------:|
| Active members | 1 |
| Monthly revenue | 8.99 EUR |
| Monthly cost | 0.00 EUR |
| Cost per member | 0.00 EUR |
| Gross margin | 8.99 EUR |
| Gross margin % | 100.0% |
| Member payments (gross) | 8.99 EUR |
| Platform cost | 72.58 EUR |
| Platform cost per member | 72.58 EUR |
| Period gross margin | -63.59 EUR |
| Period gross margin % | -707.3% |
| Period net liquidity | -63.97 EUR (burning) |
_Revenue source: manual_
## Liquidity & Budget (through 2026-06)
| Metric | Value |
|--------|------:|
| Initial budget | 1000.00 EUR |
| Cumulative member payments (net) | 68.88 EUR |
| Cumulative platform cost | 1158.24 EUR |
| Cumulative net liquidity | -1089.36 EUR (burning) |
| Remaining budget | -89.36 EUR (over budget) |
| Months tracked | 16 |
## Monthly History
| Period | Members | Gross revenue | Platform cost | Net liquidity |
|--------|--------:|--------------:|--------------:|--------------:|
| 2025-03 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-04 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-05 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-06 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-07 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-08 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-09 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-10 | 0 | 0.00 | 72.20 | -72.20 |
| 2025-11 | 1 | 8.99 | 72.58 | -63.97 |
| 2025-12 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-01 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-02 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-03 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-04 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-05 | 1 | 8.99 | 72.58 | -63.97 |
| 2026-06 | 1 | 8.99 | 72.58 | -63.97 |
## Pricing Model Registry
| ID | Name | Type | Status |
@@ -28,7 +64,9 @@ _Revenue source: manual_
## Registries Loaded
- Product model (`data/product.json`)
- Budget (`data/budget.json`)
- Pricing model registry (`data/pricing-models.json`)
- Cost registry (`data/costs.json`)
- Revenue registry (`data/revenue.json`)
- Membership registry (`data/membership.json`)
- Platform costs (`data/costs.json`)
- Member payments (`data/revenue.json`)
- Membership (`data/membership.json`)
- Requirements (`REQUIREMENTS.md`)

View File

@@ -3,10 +3,17 @@ from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from observatory.economics import active_members, build_snapshot, monthly_cost_total
from observatory.economics import (
active_members,
build_liquidity_summary,
build_snapshot,
monthly_cost_from_rate_card,
)
from observatory.load import (
load_costs,
load_budget,
load_cost_rate_card,
load_membership,
load_monthly_platform_costs,
load_pricing_models,
load_product,
load_revenue,
@@ -20,36 +27,58 @@ def test_active_members_counts_only_active_status() -> None:
assert active_members(members) == 1
def test_build_snapshot_reflects_sole_member_and_zero_costs() -> None:
def test_rate_card_matches_documented_platform_cost_without_revenue() -> None:
rate_card, fx_rates = load_cost_rate_card(DATA_DIR)
total = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("0"), 0)
assert total == Decimal("72.20")
def test_rate_card_matches_platform_cost_with_one_member() -> None:
rate_card, fx_rates = load_cost_rate_card(DATA_DIR)
total = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("8.99"), 1)
assert total == Decimal("72.58")
def test_build_snapshot_june_2026_shows_liquidity_burn() -> None:
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
revenue = load_revenue(DATA_DIR)
_, costs, fx_rates = load_costs(DATA_DIR)
monthly_costs = load_monthly_platform_costs(DATA_DIR)
snapshot = build_snapshot("2026-06", product, models, members, revenue, costs, fx_rates)
snapshot = build_snapshot("2026-06", product, models, members, revenue, monthly_costs)
assert snapshot.active_members == 1
assert snapshot.monthly_revenue == Decimal("8.99")
assert snapshot.revenue_source == "manual"
assert snapshot.pricing_model_count == 3
assert snapshot.monthly_cost == Decimal("0.00")
assert snapshot.cost_per_member == Decimal("0.00")
assert snapshot.gross_margin == Decimal("8.99")
assert snapshot.gross_margin_pct == Decimal("100.0")
assert snapshot.monthly_platform_cost == Decimal("72.58")
assert snapshot.period_net_liquidity == Decimal("-63.97")
assert snapshot.liquidity_status == "burning"
assert snapshot.gross_margin == Decimal("-63.59")
def test_monthly_cost_is_zero_with_empty_registry() -> None:
_, costs, fx_rates = load_costs(DATA_DIR)
total = monthly_cost_total(costs, fx_rates, Decimal("8.99"), 1)
assert total == Decimal("0.00")
def test_liquidity_summary_tracks_budget_burn_through_june_2026() -> None:
budget = load_budget(DATA_DIR)
revenue = load_revenue(DATA_DIR)
monthly_costs = load_monthly_platform_costs(DATA_DIR)
summary = build_liquidity_summary(budget, revenue, monthly_costs, "2026-06")
assert summary.initial_budget == Decimal("1000.00")
assert summary.cumulative_platform_cost == Decimal("1158.24")
assert summary.cumulative_member_payments == Decimal("68.88")
assert summary.cumulative_net_liquidity == Decimal("-1089.36")
assert summary.remaining_budget == Decimal("-89.36")
assert summary.liquidity_status == "burning"
assert summary.months_tracked == 16
def test_dashboard_module_renders_markdown() -> None:
def test_dashboard_renders_liquidity_sections() -> None:
from observatory.dashboard import generate_dashboard
report = generate_dashboard(DATA_DIR, "2026-06")
assert "# Economics Dashboard v1" in report
assert "Pricing Model Registry" in report
assert "flat-899-eur-monthly" in report
assert "Active members | 1" in report
assert "Liquidity & Budget" in report
assert "Monthly History" in report
assert "2025-03" in report
assert "2025-11" in report
assert "remaining budget" not in report.lower() or "Remaining budget" in report
assert "-89.36" in report