from __future__ import annotations from decimal import Decimal, ROUND_HALF_UP from .models import ( Budget, ExpenseRecord, MembershipRecord, MonthlyPlatformCost, PaymentRecord, ) TWOPLACES = Decimal("0.01") def _quantize(value: Decimal) -> Decimal: return value.quantize(TWOPLACES, 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")) raise ValueError(f"unsupported expense currency: {currency}") def aggregate_infrastructure_by_period( expenses: list[ExpenseRecord], fx_rates: dict[str, Decimal], reporting_currency: str = "EUR", ) -> dict[str, Decimal]: if reporting_currency != "EUR": raise ValueError("only EUR reporting currency is supported") totals: dict[str, Decimal] = {} for record in expenses: if record.cost_class != "infrastructure": continue totals[record.period] = totals.get(record.period, Decimal("0")) + convert_to_eur( record.amount, record.currency, fx_rates ) return {period: _quantize(total) for period, total in totals.items()} def payment_processing_by_period(payments: list[PaymentRecord]) -> dict[str, Decimal]: totals: dict[str, Decimal] = {} for record in payments: totals[record.period] = totals.get(record.period, Decimal("0")) + record.fees_amount return {period: _quantize(total) for period, total in totals.items()} def _period_sort_key(period: str) -> tuple[int, int]: year, month = period.split("-") return int(year), int(month) def periods_from_budget_through_latest( budget: Budget, expenses: list[ExpenseRecord], payments: list[PaymentRecord], ) -> list[str]: candidates = {budget.started} candidates.update(record.period for record in expenses) candidates.update(record.period for record in payments) latest = max(candidates, key=_period_sort_key) periods: list[str] = [] year, month = map(int, budget.started.split("-")) end_year, end_month = map(int, latest.split("-")) while (year, month) <= (end_year, end_month): periods.append(f"{year}-{month:02d}") month += 1 if month > 12: month = 1 year += 1 return periods def active_members_for_period(period: str, members: list[MembershipRecord]) -> int: period_start = f"{period}-01" count = 0 for member in members: if member.joined_at[:7] > period: continue if member.churned_at and member.churned_at[:7] < period: continue if member.status == "active" or ( member.churned_at and member.churned_at[:7] >= period ): count += 1 return count def build_monthly_ledger( budget: Budget, expenses: list[ExpenseRecord], payments: list[PaymentRecord], members: list[MembershipRecord], fx_rates: dict[str, Decimal], ) -> list[MonthlyPlatformCost]: infrastructure = aggregate_infrastructure_by_period(expenses, fx_rates) processing = payment_processing_by_period(payments) payment_by_period = {record.period: record for record in payments} rows: list[MonthlyPlatformCost] = [] for period in periods_from_budget_through_latest(budget, expenses, payments): payment = payment_by_period.get(period) rows.append( MonthlyPlatformCost( period=period, infrastructure_cost=infrastructure.get(period, Decimal("0.00")), payment_processing_cost=processing.get(period, Decimal("0.00")), active_members=( payment.member_count if payment and payment.member_count else active_members_for_period(period, members) ), gross_revenue=payment.gross_amount if payment else Decimal("0.00"), ) ) return rows