generated from coulomb/repo-seed
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.
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
|
|
from .models import (
|
|
Budget,
|
|
EconomicsSnapshot,
|
|
LiquidityStatus,
|
|
LiquiditySummary,
|
|
MembershipRecord,
|
|
MonthlyPlatformCost,
|
|
PaymentRecord,
|
|
PricingModel,
|
|
Product,
|
|
)
|
|
|
|
TWOPLACES = Decimal("0.01")
|
|
PCTPLACES = Decimal("0.1")
|
|
|
|
|
|
def _quantize(value: Decimal, exp: Decimal = TWOPLACES) -> Decimal:
|
|
return value.quantize(exp, rounding=ROUND_HALF_UP)
|
|
|
|
|
|
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")
|
|
|
|
|
|
def active_pricing_model(models: list[PricingModel], product: Product) -> PricingModel:
|
|
for model in models:
|
|
if model.id == product.active_pricing_model_id:
|
|
return model
|
|
raise ValueError(f"active pricing model not found: {product.active_pricing_model_id}")
|
|
|
|
|
|
def payment_for_period(period: str, payments: list[PaymentRecord]) -> PaymentRecord | None:
|
|
for record in payments:
|
|
if record.period == period:
|
|
return record
|
|
return None
|
|
|
|
|
|
def estimate_monthly_revenue(
|
|
period: str,
|
|
product: Product,
|
|
models: list[PricingModel],
|
|
members: list[MembershipRecord],
|
|
payments: list[PaymentRecord],
|
|
monthly_costs: list[MonthlyPlatformCost],
|
|
) -> tuple[Decimal, Decimal, str]:
|
|
recorded = payment_for_period(period, payments)
|
|
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_ledger"
|
|
|
|
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,
|
|
payments: list[PaymentRecord],
|
|
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_infrastructure = Decimal("0")
|
|
cumulative_processing = Decimal("0")
|
|
for period in tracked:
|
|
month = cost_by_period[period]
|
|
cumulative_infrastructure += month.infrastructure_cost
|
|
cumulative_processing += month.payment_processing_cost
|
|
payment = payment_for_period(period, payments)
|
|
cumulative_payments += payment.net_amount if payment else Decimal("0")
|
|
|
|
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_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),
|
|
months_tracked=len(tracked),
|
|
)
|
|
|
|
|
|
def build_snapshot(
|
|
period: str,
|
|
product: Product,
|
|
models: list[PricingModel],
|
|
members: list[MembershipRecord],
|
|
payments: list[PaymentRecord],
|
|
monthly_costs: list[MonthlyPlatformCost],
|
|
) -> EconomicsSnapshot:
|
|
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, payments, monthly_costs
|
|
)
|
|
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 total_platform else Decimal("0.0")
|
|
)
|
|
period_net = _quantize(net_revenue - infrastructure)
|
|
|
|
return EconomicsSnapshot(
|
|
period=period,
|
|
currency=product.currency,
|
|
active_members=count,
|
|
monthly_revenue=_quantize(gross_revenue),
|
|
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,
|
|
pricing_model_count=len(models),
|
|
revenue_source=revenue_source,
|
|
period_net_liquidity=period_net,
|
|
liquidity_status=liquidity_status_for(period_net),
|
|
) |