generated from coulomb/repo-seed
Refactor economics to expense-record ledger with correct Bubble cost
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.
This commit is contained in:
@@ -4,15 +4,14 @@ from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from .models import (
|
||||
Budget,
|
||||
CostEntry,
|
||||
EconomicsSnapshot,
|
||||
LiquidityStatus,
|
||||
LiquiditySummary,
|
||||
MembershipRecord,
|
||||
MonthlyPlatformCost,
|
||||
PaymentRecord,
|
||||
PricingModel,
|
||||
Product,
|
||||
RevenueEntry,
|
||||
)
|
||||
|
||||
TWOPLACES = Decimal("0.01")
|
||||
@@ -23,16 +22,6 @@ def _quantize(value: Decimal, exp: Decimal = TWOPLACES) -> Decimal:
|
||||
return value.quantize(exp, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _convert_to_eur(amount: Decimal, currency: str, fx_rates: dict[str, Decimal]) -> Decimal:
|
||||
if currency == "EUR":
|
||||
return amount
|
||||
if currency == "USD":
|
||||
return amount * fx_rates.get("USD/EUR", Decimal("1"))
|
||||
if currency == "ratio":
|
||||
return amount
|
||||
raise ValueError(f"unsupported currency for conversion: {currency}")
|
||||
|
||||
|
||||
def liquidity_status_for(net: Decimal) -> LiquidityStatus:
|
||||
if net < Decimal("0"):
|
||||
return "burning"
|
||||
@@ -52,27 +41,10 @@ def active_pricing_model(models: list[PricingModel], product: Product) -> Pricin
|
||||
raise ValueError(f"active pricing model not found: {product.active_pricing_model_id}")
|
||||
|
||||
|
||||
def monthly_cost_from_rate_card(
|
||||
cost_entries: list[CostEntry],
|
||||
fx_rates: dict[str, Decimal],
|
||||
gross_revenue: Decimal,
|
||||
member_count: int,
|
||||
) -> Decimal:
|
||||
total = Decimal("0")
|
||||
for entry in cost_entries:
|
||||
if entry.allocation == "flat":
|
||||
total += _convert_to_eur(entry.amount, entry.currency, fx_rates)
|
||||
elif entry.allocation == "percent_of_gross_revenue":
|
||||
total += gross_revenue * entry.amount
|
||||
elif entry.allocation == "per_active_member":
|
||||
total += _convert_to_eur(entry.amount, entry.currency, fx_rates) * member_count
|
||||
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
|
||||
def payment_for_period(period: str, payments: list[PaymentRecord]) -> PaymentRecord | None:
|
||||
for record in payments:
|
||||
if record.period == period:
|
||||
return record
|
||||
return None
|
||||
|
||||
|
||||
@@ -81,16 +53,16 @@ def estimate_monthly_revenue(
|
||||
product: Product,
|
||||
models: list[PricingModel],
|
||||
members: list[MembershipRecord],
|
||||
revenue_entries: list[RevenueEntry],
|
||||
payments: list[PaymentRecord],
|
||||
monthly_costs: list[MonthlyPlatformCost],
|
||||
) -> tuple[Decimal, Decimal, str]:
|
||||
recorded = revenue_for_period(period, revenue_entries)
|
||||
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_platform_history"
|
||||
return month.gross_revenue, month.gross_revenue, "derived_from_ledger"
|
||||
|
||||
model = active_pricing_model(models, product)
|
||||
count = active_members(members)
|
||||
@@ -104,7 +76,7 @@ def periods_through(target: str, monthly_costs: list[MonthlyPlatformCost]) -> li
|
||||
|
||||
def build_liquidity_summary(
|
||||
budget: Budget,
|
||||
revenue_entries: list[RevenueEntry],
|
||||
payments: list[PaymentRecord],
|
||||
monthly_costs: list[MonthlyPlatformCost],
|
||||
through_period: str,
|
||||
) -> LiquiditySummary:
|
||||
@@ -118,11 +90,9 @@ def build_liquidity_summary(
|
||||
month = cost_by_period[period]
|
||||
cumulative_infrastructure += month.infrastructure_cost
|
||||
cumulative_processing += month.payment_processing_cost
|
||||
payment = revenue_for_period(period, revenue_entries)
|
||||
payment = payment_for_period(period, payments)
|
||||
cumulative_payments += payment.net_amount if payment else Decimal("0")
|
||||
|
||||
# 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)
|
||||
@@ -147,13 +117,13 @@ def build_snapshot(
|
||||
product: Product,
|
||||
models: list[PricingModel],
|
||||
members: list[MembershipRecord],
|
||||
revenue_entries: list[RevenueEntry],
|
||||
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, revenue_entries, monthly_costs
|
||||
period, product, models, members, payments, monthly_costs
|
||||
)
|
||||
infrastructure = month.infrastructure_cost
|
||||
processing = month.payment_processing_cost
|
||||
|
||||
Reference in New Issue
Block a user