Fix cumulative platform cost Stripe double-counting

Split infrastructure vs payment-processing costs. Liquidity burn now
uses infrastructure cash out only (€1,155.20 cumulative) because Stripe
fees are already deducted from net member payments. Total platform cost
(€1,158.24) remains visible for gross-margin economics.
This commit is contained in:
2026-06-22 01:51:53 +02:00
parent fe2174f37a
commit ea2c2c6403
8 changed files with 143 additions and 97 deletions

View File

@@ -24,18 +24,27 @@ separate from platform cost accrual.
### LQ-003 — Budget tracking ### LQ-003 — Budget tracking
Maintain an operator liquidity budget (initial: **€1,000**) and compute Maintain an operator liquidity budget (initial: **€1,000**) and compute
remaining budget after cumulative platform spend minus cumulative member remaining budget after cumulative **infrastructure** spend minus cumulative net
payments received. member payments received.
### LQ-004 — Liquidity position ### LQ-004 — Liquidity position
Report whether the project is **burning**, **neutral**, or **generating** Report whether the project is **burning**, **neutral**, or **generating**
liquidity each period: liquidity each period:
- `period_net = member_payments - platform_costs` - `period_net = net_member_payments - infrastructure_cost`
- `cumulative_net = sum(period_net)` - `cumulative_net = sum(period_net)`
- `remaining_budget = initial_budget + cumulative_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 Negative remaining budget means the MVP has consumed more liquidity than the
allocated budget. allocated budget.

View File

@@ -51,22 +51,22 @@
} }
], ],
"monthly_history": [ "monthly_history": [
{"period": "2025-03", "platform_cost": "72.20", "active_members": 0, "gross_revenue": "0.00"}, {"period": "2025-03", "infrastructure_cost": "72.20", "payment_processing_cost": "0.00", "active_members": 0, "gross_revenue": "0.00"},
{"period": "2025-04", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.20", "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", "platform_cost": "72.58", "active_members": 1, "gross_revenue": "8.99"}, {"period": "2025-11", "infrastructure_cost": "72.20", "payment_processing_cost": "0.38", "active_members": 1, "gross_revenue": "8.99"},
{"period": "2025-12", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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", "platform_cost": "72.58", "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."
} }

View File

@@ -30,10 +30,11 @@ def _history_rows(
continue continue
payment = revenue_by_period.get(month.period) payment = revenue_by_period.get(month.period)
net_payment = payment.net_amount if payment else Decimal("0") 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( lines.append(
f"| {month.period} | {month.active_members} | {month.gross_revenue} | " 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) return "\n".join(lines)
@@ -73,12 +74,15 @@ def render_dashboard(
|--------|------:| |--------|------:|
| Active members | {snapshot.active_members} | | Active members | {snapshot.active_members} |
| Member payments (gross) | {snapshot.monthly_revenue} {snapshot.currency} | | 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} | | Platform cost per member | {snapshot.cost_per_member} {snapshot.currency} |
| Period gross margin | {snapshot.gross_margin} {snapshot.currency} | | Period gross margin | {snapshot.gross_margin} {snapshot.currency} |
| Period gross margin % | {snapshot.gross_margin_pct}% | | Period gross margin % | {snapshot.gross_margin_pct}% |
| Period net liquidity | {snapshot.period_net_liquidity} {snapshot.currency} ({snapshot.liquidity_status}) | | 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}_ _Revenue source: {snapshot.revenue_source}_
## Liquidity & Budget (through {liquidity.through_period}) ## Liquidity & Budget (through {liquidity.through_period})
@@ -87,15 +91,17 @@ _Revenue source: {snapshot.revenue_source}_
|--------|------:| |--------|------:|
| Initial budget | {liquidity.initial_budget} {liquidity.currency} | | Initial budget | {liquidity.initial_budget} {liquidity.currency} |
| Cumulative member payments (net) | {liquidity.cumulative_member_payments} {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}) | | Cumulative net liquidity | {liquidity.cumulative_net_liquidity} {liquidity.currency} ({liquidity.liquidity_status}) |
| Remaining budget | {liquidity.remaining_budget} {liquidity.currency} ({budget_state}) | | Remaining budget | {liquidity.remaining_budget} {liquidity.currency} ({budget_state}) |
| Months tracked | {liquidity.months_tracked} | | Months tracked | {liquidity.months_tracked} |
## Monthly History ## Monthly History
| Period | Members | Gross revenue | Platform cost | Net liquidity | | Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity |
|--------|--------:|--------------:|--------------:|--------------:| |--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:|
{history_lines} {history_lines}
## Pricing Model Registry ## Pricing Model Registry

View File

@@ -112,22 +112,29 @@ def build_liquidity_summary(
cost_by_period = {item.period: item for item in monthly_costs} cost_by_period = {item.period: item for item in monthly_costs}
cumulative_payments = Decimal("0") cumulative_payments = Decimal("0")
cumulative_cost = Decimal("0") cumulative_infrastructure = Decimal("0")
cumulative_processing = Decimal("0")
for period in tracked: for period in tracked:
month = cost_by_period[period] 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) payment = revenue_for_period(period, revenue_entries)
cumulative_payments += payment.net_amount if payment else Decimal("0") 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) remaining = _quantize(budget.initial_budget + cumulative_net)
cumulative_total = _quantize(cumulative_infrastructure + cumulative_processing)
return LiquiditySummary( return LiquiditySummary(
currency=budget.currency, currency=budget.currency,
through_period=through_period, through_period=through_period,
initial_budget=budget.initial_budget, initial_budget=budget.initial_budget,
cumulative_member_payments=_quantize(cumulative_payments), 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, cumulative_net_liquidity=cumulative_net,
remaining_budget=remaining, remaining_budget=remaining,
liquidity_status=liquidity_status_for(cumulative_net), liquidity_status=liquidity_status_for(cumulative_net),
@@ -148,22 +155,26 @@ def build_snapshot(
gross_revenue, net_revenue, revenue_source = estimate_monthly_revenue( gross_revenue, net_revenue, revenue_source = estimate_monthly_revenue(
period, product, models, members, revenue_entries, monthly_costs period, product, models, members, revenue_entries, monthly_costs
) )
platform_cost = month.platform_cost infrastructure = month.infrastructure_cost
cost_per_member = _quantize(platform_cost / count) if count else Decimal("0.00") processing = month.payment_processing_cost
gross_margin = _quantize(gross_revenue - platform_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 = ( margin_pct = (
_quantize((gross_margin / gross_revenue) * Decimal("100"), PCTPLACES) _quantize((gross_margin / gross_revenue) * Decimal("100"), PCTPLACES)
if gross_revenue 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( return EconomicsSnapshot(
period=period, period=period,
currency=product.currency, currency=product.currency,
active_members=count, active_members=count,
monthly_revenue=_quantize(gross_revenue), 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, cost_per_member=cost_per_member,
gross_margin=gross_margin, gross_margin=gross_margin,
gross_margin_pct=margin_pct, gross_margin_pct=margin_pct,

View File

@@ -4,6 +4,8 @@ import json
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
_ZERO = Decimal("0")
from .models import ( from .models import (
Budget, Budget,
CostEntry, 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]: def load_monthly_platform_costs(data_dir: Path | None = None) -> list[MonthlyPlatformCost]:
raw = _read_json((data_dir or default_data_dir()) / "costs.json") raw = _read_json((data_dir or default_data_dir()) / "costs.json")
return [ rows: list[MonthlyPlatformCost] = []
MonthlyPlatformCost( for item in raw.get("monthly_history", []):
period=item["period"], if "infrastructure_cost" in item:
platform_cost=_money(item["platform_cost"]), infrastructure = _money(item["infrastructure_cost"])
active_members=item["active_members"], processing = _money(item.get("payment_processing_cost", "0"))
gross_revenue=_money(item["gross_revenue"]), 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]: def load_revenue(data_dir: Path | None = None) -> list[RevenueEntry]:

View File

@@ -46,10 +46,15 @@ class CostEntry:
@dataclass(frozen=True) @dataclass(frozen=True)
class MonthlyPlatformCost: class MonthlyPlatformCost:
period: str period: str
platform_cost: Decimal infrastructure_cost: Decimal
payment_processing_cost: Decimal
active_members: int active_members: int
gross_revenue: Decimal gross_revenue: Decimal
@property
def total_platform_cost(self) -> Decimal:
return self.infrastructure_cost + self.payment_processing_cost
@dataclass(frozen=True) @dataclass(frozen=True)
class Budget: class Budget:
@@ -85,7 +90,9 @@ class EconomicsSnapshot:
currency: str currency: str
active_members: int active_members: int
monthly_revenue: Decimal 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 cost_per_member: Decimal
gross_margin: Decimal gross_margin: Decimal
gross_margin_pct: Decimal gross_margin_pct: Decimal
@@ -101,7 +108,9 @@ class LiquiditySummary:
through_period: str through_period: str
initial_budget: Decimal initial_budget: Decimal
cumulative_member_payments: 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 cumulative_net_liquidity: Decimal
remaining_budget: Decimal remaining_budget: Decimal
liquidity_status: LiquidityStatus liquidity_status: LiquidityStatus

View File

@@ -13,12 +13,15 @@
|--------|------:| |--------|------:|
| Active members | 1 | | Active members | 1 |
| Member payments (gross) | 8.99 EUR | | 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 | | Platform cost per member | 72.58 EUR |
| Period gross margin | -63.59 EUR | | Period gross margin | -63.59 EUR |
| Period gross margin % | -707.3% | | 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_ _Revenue source: manual_
## Liquidity & Budget (through 2026-06) ## Liquidity & Budget (through 2026-06)
@@ -27,31 +30,33 @@ _Revenue source: manual_
|--------|------:| |--------|------:|
| Initial budget | 1000.00 EUR | | Initial budget | 1000.00 EUR |
| Cumulative member payments (net) | 68.88 EUR | | Cumulative member payments (net) | 68.88 EUR |
| Cumulative platform cost | 1158.24 EUR | | Cumulative infrastructure cost | 1155.20 EUR |
| Cumulative net liquidity | -1089.36 EUR (burning) | | Cumulative payment processing | 3.04 EUR |
| Remaining budget | -89.36 EUR (over budget) | | Cumulative total platform cost | 1158.24 EUR |
| Cumulative net liquidity | -1086.32 EUR (burning) |
| Remaining budget | -86.32 EUR (over budget) |
| Months tracked | 16 | | Months tracked | 16 |
## Monthly History ## Monthly History
| Period | Members | Gross revenue | Platform cost | Net liquidity | | Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity |
|--------|--------:|--------------:|--------------:|--------------:| |--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:|
| 2025-03 | 0 | 0.00 | 72.20 | -72.20 | | 2025-03 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 |
| 2025-04 | 0 | 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 | -72.20 | | 2025-05 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 |
| 2025-06 | 0 | 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 | -72.20 | | 2025-07 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 |
| 2025-08 | 0 | 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 | -72.20 | | 2025-09 | 0 | 0.00 | 72.20 | 0.00 | 72.20 | -72.20 |
| 2025-10 | 0 | 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.58 | -63.97 | | 2025-11 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2025-12 | 1 | 8.99 | 72.58 | -63.97 | | 2025-12 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-01 | 1 | 8.99 | 72.58 | -63.97 | | 2026-01 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-02 | 1 | 8.99 | 72.58 | -63.97 | | 2026-02 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-03 | 1 | 8.99 | 72.58 | -63.97 | | 2026-03 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-04 | 1 | 8.99 | 72.58 | -63.97 | | 2026-04 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-05 | 1 | 8.99 | 72.58 | -63.97 | | 2026-05 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
| 2026-06 | 1 | 8.99 | 72.58 | -63.97 | | 2026-06 | 1 | 8.99 | 72.20 | 0.38 | 72.58 | -63.59 |
## Pricing Model Registry ## Pricing Model Registry

View File

@@ -27,19 +27,16 @@ def test_active_members_counts_only_active_status() -> None:
assert active_members(members) == 1 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) rate_card, fx_rates = load_cost_rate_card(DATA_DIR)
total = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("0"), 0) infrastructure = monthly_cost_from_rate_card(rate_card, fx_rates, Decimal("0"), 0)
assert total == Decimal("72.20") 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: def test_build_snapshot_june_2026_avoids_stripe_double_count_in_liquidity() -> 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) product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR) models = load_pricing_models(DATA_DIR)
members = load_membership(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) snapshot = build_snapshot("2026-06", product, models, members, revenue, monthly_costs)
assert snapshot.active_members == 1 assert snapshot.monthly_infrastructure_cost == Decimal("72.20")
assert snapshot.monthly_revenue == Decimal("8.99") assert snapshot.monthly_payment_processing_cost == Decimal("0.38")
assert snapshot.monthly_platform_cost == Decimal("72.58") assert snapshot.monthly_total_platform_cost == Decimal("72.58")
assert snapshot.period_net_liquidity == Decimal("-63.97") assert snapshot.period_net_liquidity == Decimal("-63.59")
assert snapshot.liquidity_status == "burning"
assert snapshot.gross_margin == 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) budget = load_budget(DATA_DIR)
revenue = load_revenue(DATA_DIR) revenue = load_revenue(DATA_DIR)
monthly_costs = load_monthly_platform_costs(DATA_DIR) monthly_costs = load_monthly_platform_costs(DATA_DIR)
summary = build_liquidity_summary(budget, revenue, monthly_costs, "2026-06") summary = build_liquidity_summary(budget, revenue, monthly_costs, "2026-06")
assert summary.initial_budget == Decimal("1000.00") assert summary.cumulative_infrastructure_cost == Decimal("1155.20")
assert summary.cumulative_platform_cost == Decimal("1158.24") 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_member_payments == Decimal("68.88")
assert summary.cumulative_net_liquidity == Decimal("-1089.36") assert summary.cumulative_net_liquidity == Decimal("-1086.32")
assert summary.remaining_budget == Decimal("-89.36") assert summary.remaining_budget == Decimal("-86.32")
assert summary.liquidity_status == "burning" assert summary.liquidity_status == "burning"
assert summary.months_tracked == 16 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 from observatory.dashboard import generate_dashboard
report = generate_dashboard(DATA_DIR, "2026-06") report = generate_dashboard(DATA_DIR, "2026-06")
assert "Liquidity & Budget" in report assert "Cumulative infrastructure cost" in report
assert "Monthly History" in report assert "1155.20" in report
assert "2025-03" in report assert "-86.32" in report
assert "2025-11" in report
assert "remaining budget" not in report.lower() or "Remaining budget" in report
assert "-89.36" in report