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

@@ -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

View File

@@ -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,

View File

@@ -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]:

View File

@@ -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