diff --git a/projects/coulomb-pricing/REQUIREMENTS.md b/projects/coulomb-pricing/REQUIREMENTS.md index f9ce3d4..9bc7af3 100644 --- a/projects/coulomb-pricing/REQUIREMENTS.md +++ b/projects/coulomb-pricing/REQUIREMENTS.md @@ -24,18 +24,27 @@ 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. +remaining budget after cumulative **infrastructure** spend minus cumulative net +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` +- `period_net = net_member_payments - infrastructure_cost` - `cumulative_net = sum(period_net)` - `remaining_budget = initial_budget + cumulative_net` +**No double-counting:** payment-processing fees (Stripe) are deducted from net +member payments. They are tracked separately for economics reporting but must +**not** be subtracted again in the liquidity formula. + +- `total_platform_cost = infrastructure_cost + payment_processing_cost` (for + gross-margin economics vs gross revenue) +- `cumulative_total_platform_cost` is informational; liquidity burn uses + `cumulative_infrastructure_cost` only + Negative remaining budget means the MVP has consumed more liquidity than the allocated budget. diff --git a/projects/coulomb-pricing/data/costs.json b/projects/coulomb-pricing/data/costs.json index 480220c..4edf9af 100644 --- a/projects/coulomb-pricing/data/costs.json +++ b/projects/coulomb-pricing/data/costs.json @@ -51,22 +51,22 @@ } ], "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"} + {"period": "2025-03", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-04", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-05", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-06", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-07", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-08", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-09", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-10", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"}, + {"period": "2025-11", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2025-12", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-01", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-02", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-03", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-04", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-05", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"}, + {"period": "2026-06", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"} ], - "note": "Platform costs are operator expenses. Customer cost-pass-through billing is not active yet." + "note": "Infrastructure costs are operator cash outflows. Payment processing is tracked separately and already deducted from net member payments — do not double-count in liquidity." } \ No newline at end of file diff --git a/projects/coulomb-pricing/observatory/dashboard.py b/projects/coulomb-pricing/observatory/dashboard.py index ea7bf3d..28489fd 100644 --- a/projects/coulomb-pricing/observatory/dashboard.py +++ b/projects/coulomb-pricing/observatory/dashboard.py @@ -30,10 +30,11 @@ def _history_rows( 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 + period_net = net_payment - month.infrastructure_cost lines.append( f"| {month.period} | {month.active_members} | {month.gross_revenue} | " - f"{month.platform_cost} | {period_net:.2f} |" + f"{month.infrastructure_cost} | {month.payment_processing_cost} | " + f"{month.total_platform_cost} | {period_net:.2f} |" ) return "\n".join(lines) @@ -73,12 +74,15 @@ def render_dashboard( |--------|------:| | Active members | {snapshot.active_members} | | Member payments (gross) | {snapshot.monthly_revenue} {snapshot.currency} | -| Platform cost | {snapshot.monthly_platform_cost} {snapshot.currency} | +| Infrastructure cost | {snapshot.monthly_infrastructure_cost} {snapshot.currency} | +| Payment processing cost | {snapshot.monthly_payment_processing_cost} {snapshot.currency} | +| Total platform cost | {snapshot.monthly_total_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}) | +_Period net liquidity = net member payments − infrastructure cost (processing fees already netted from payments)._ _Revenue source: {snapshot.revenue_source}_ ## Liquidity & Budget (through {liquidity.through_period}) @@ -87,15 +91,17 @@ _Revenue source: {snapshot.revenue_source}_ |--------|------:| | 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 infrastructure cost | {liquidity.cumulative_infrastructure_cost} {liquidity.currency} | +| Cumulative payment processing | {liquidity.cumulative_payment_processing_cost} {liquidity.currency} | +| Cumulative total platform cost | {liquidity.cumulative_total_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 | -|--------|--------:|--------------:|--------------:|--------------:| +| Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity | +|--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:| {history_lines} ## Pricing Model Registry diff --git a/projects/coulomb-pricing/observatory/economics.py b/projects/coulomb-pricing/observatory/economics.py index 1584a81..28d79a5 100644 --- a/projects/coulomb-pricing/observatory/economics.py +++ b/projects/coulomb-pricing/observatory/economics.py @@ -112,22 +112,29 @@ def build_liquidity_summary( cost_by_period = {item.period: item for item in monthly_costs} cumulative_payments = Decimal("0") - cumulative_cost = Decimal("0") + cumulative_infrastructure = Decimal("0") + cumulative_processing = Decimal("0") for period in tracked: month = cost_by_period[period] - cumulative_cost += month.platform_cost + cumulative_infrastructure += month.infrastructure_cost + cumulative_processing += month.payment_processing_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) + # Liquidity uses net member payments vs infrastructure cash out only. + # Payment-processing fees are already deducted from net payments. + cumulative_net = _quantize(cumulative_payments - cumulative_infrastructure) remaining = _quantize(budget.initial_budget + cumulative_net) + cumulative_total = _quantize(cumulative_infrastructure + cumulative_processing) 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_infrastructure_cost=_quantize(cumulative_infrastructure), + cumulative_payment_processing_cost=_quantize(cumulative_processing), + cumulative_total_platform_cost=cumulative_total, cumulative_net_liquidity=cumulative_net, remaining_budget=remaining, liquidity_status=liquidity_status_for(cumulative_net), @@ -148,22 +155,26 @@ def build_snapshot( gross_revenue, net_revenue, revenue_source = estimate_monthly_revenue( period, product, models, members, revenue_entries, monthly_costs ) - 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) + infrastructure = month.infrastructure_cost + processing = month.payment_processing_cost + total_platform = month.total_platform_cost + cost_per_member = _quantize(total_platform / count) if count else Decimal("0.00") + gross_margin = _quantize(gross_revenue - total_platform) margin_pct = ( _quantize((gross_margin / gross_revenue) * Decimal("100"), PCTPLACES) if gross_revenue - else Decimal("-100.0") if platform_cost else Decimal("0.0") + else Decimal("-100.0") if total_platform else Decimal("0.0") ) - period_net = _quantize(net_revenue - platform_cost) + period_net = _quantize(net_revenue - infrastructure) return EconomicsSnapshot( period=period, currency=product.currency, active_members=count, monthly_revenue=_quantize(gross_revenue), - monthly_platform_cost=platform_cost, + monthly_infrastructure_cost=infrastructure, + monthly_payment_processing_cost=processing, + monthly_total_platform_cost=total_platform, cost_per_member=cost_per_member, gross_margin=gross_margin, gross_margin_pct=margin_pct, diff --git a/projects/coulomb-pricing/observatory/load.py b/projects/coulomb-pricing/observatory/load.py index 99282b3..bbc9296 100644 --- a/projects/coulomb-pricing/observatory/load.py +++ b/projects/coulomb-pricing/observatory/load.py @@ -4,6 +4,8 @@ import json from decimal import Decimal from pathlib import Path +_ZERO = Decimal("0") + from .models import ( Budget, CostEntry, @@ -89,15 +91,25 @@ def load_cost_rate_card(data_dir: Path | None = None) -> tuple[list[CostEntry], 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"]), + 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"]), + ) ) - for item in raw.get("monthly_history", []) - ] + return rows def load_revenue(data_dir: Path | None = None) -> list[RevenueEntry]: diff --git a/projects/coulomb-pricing/observatory/models.py b/projects/coulomb-pricing/observatory/models.py index 2f176d8..ea33d3e 100644 --- a/projects/coulomb-pricing/observatory/models.py +++ b/projects/coulomb-pricing/observatory/models.py @@ -46,10 +46,15 @@ class CostEntry: @dataclass(frozen=True) class MonthlyPlatformCost: period: str - platform_cost: Decimal + infrastructure_cost: Decimal + payment_processing_cost: Decimal active_members: int gross_revenue: Decimal + @property + def total_platform_cost(self) -> Decimal: + return self.infrastructure_cost + self.payment_processing_cost + @dataclass(frozen=True) class Budget: @@ -85,7 +90,9 @@ class EconomicsSnapshot: currency: str active_members: int monthly_revenue: Decimal - monthly_platform_cost: Decimal + monthly_infrastructure_cost: Decimal + monthly_payment_processing_cost: Decimal + monthly_total_platform_cost: Decimal cost_per_member: Decimal gross_margin: Decimal gross_margin_pct: Decimal @@ -101,7 +108,9 @@ class LiquiditySummary: through_period: str initial_budget: Decimal cumulative_member_payments: Decimal - cumulative_platform_cost: Decimal + cumulative_infrastructure_cost: Decimal + cumulative_payment_processing_cost: Decimal + cumulative_total_platform_cost: Decimal cumulative_net_liquidity: Decimal remaining_budget: Decimal liquidity_status: LiquidityStatus diff --git a/projects/coulomb-pricing/reports/economics-2026-06.md b/projects/coulomb-pricing/reports/economics-2026-06.md index f4294b9..10919a6 100644 --- a/projects/coulomb-pricing/reports/economics-2026-06.md +++ b/projects/coulomb-pricing/reports/economics-2026-06.md @@ -13,12 +13,15 @@ |--------|------:| | Active members | 1 | | Member payments (gross) | 8.99 EUR | -| Platform cost | 72.58 EUR | +| Infrastructure cost | 72.20 EUR | +| Payment processing cost | 0.38 EUR | +| Total 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) | +| Period net liquidity | -63.59 EUR (burning) | +_Period net liquidity = net member payments − infrastructure cost (processing fees already netted from payments)._ _Revenue source: manual_ ## Liquidity & Budget (through 2026-06) @@ -27,31 +30,33 @@ _Revenue source: manual_ |--------|------:| | 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) | +| Cumulative infrastructure cost | 1155.20 EUR | +| Cumulative payment processing | 3.04 EUR | +| Cumulative total platform cost | 1158.24 EUR | +| Cumulative net liquidity | -1086.32 EUR (burning) | +| Remaining budget | -86.32 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 | +| Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity | +|--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:| +| 2025-03 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-04 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-05 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-06 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-07 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-08 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-09 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-10 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 | +| 2025-11 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2025-12 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-01 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-02 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-03 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-04 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-05 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | +| 2026-06 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 | ## Pricing Model Registry diff --git a/projects/coulomb-pricing/tests/test_economics.py b/projects/coulomb-pricing/tests/test_economics.py index 2de0b14..d06089d 100644 --- a/projects/coulomb-pricing/tests/test_economics.py +++ b/projects/coulomb-pricing/tests/test_economics.py @@ -27,19 +27,16 @@ def test_active_members_counts_only_active_status() -> None: assert active_members(members) == 1 -def test_rate_card_matches_documented_platform_cost_without_revenue() -> None: +def test_rate_card_splits_infrastructure_and_payment_processing() -> 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") + infrastructure = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("0"), 0) + with_revenue = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("8.99"), 1) + assert infrastructure == Decimal("72.20") + assert with_revenue == Decimal("72.58") + assert with_revenue - infrastructure == Decimal("0.38") -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: +def test_build_snapshot_june_2026_avoids_stripe_double_count_in_liquidity() -> None: product = load_product(DATA_DIR) models = load_pricing_models(DATA_DIR) members = load_membership(DATA_DIR) @@ -48,37 +45,34 @@ def test_build_snapshot_june_2026_shows_liquidity_burn() -> None: 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.monthly_platform_cost == Decimal("72.58") - assert snapshot.period_net_liquidity == Decimal("-63.97") - assert snapshot.liquidity_status == "burning" + assert snapshot.monthly_infrastructure_cost == Decimal("72.20") + assert snapshot.monthly_payment_processing_cost == Decimal("0.38") + assert snapshot.monthly_total_platform_cost == Decimal("72.58") + assert snapshot.period_net_liquidity == Decimal("-63.59") assert snapshot.gross_margin == Decimal("-63.59") -def test_liquidity_summary_tracks_budget_burn_through_june_2026() -> None: +def test_liquidity_summary_uses_infrastructure_only_for_cumulative_burn() -> 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_infrastructure_cost == Decimal("1155.20") + assert summary.cumulative_payment_processing_cost == Decimal("3.04") + assert summary.cumulative_total_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.cumulative_net_liquidity == Decimal("-1086.32") + assert summary.remaining_budget == Decimal("-86.32") assert summary.liquidity_status == "burning" assert summary.months_tracked == 16 -def test_dashboard_renders_liquidity_sections() -> None: +def test_dashboard_renders_split_cost_columns() -> None: from observatory.dashboard import generate_dashboard report = generate_dashboard(DATA_DIR, "2026-06") - 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 \ No newline at end of file + assert "Cumulative infrastructure cost" in report + assert "1155.20" in report + assert "-86.32" in report \ No newline at end of file