# frozen_string_literal: true module Admin class CostAnalytics TREND_MONTHS = 12 def initialize(month:) @month = month.to_date.beginning_of_month end def summary build_period(@month) end def trend(months: TREND_MONTHS) start_month = @month - (months - 1).months months_list = (0...months).map { |i| start_month + i.months } months_list.map { |m| build_period(m) } end def club_breakdown range = month_range(@month) rows = session_rows(range) total_secs = rows.sum { |row| row.total_secs.to_i } total_cost_cents = PlatformCostEntry.for_month(@month).sum(:amount_cents) revenue_by_club = revenue_by_club(range) storage_by_club = storage_by_club_index club_ids = rows.map(&:club_id) clubs = Club.where(id: club_ids).includes(subscription: :plan).index_by(&:id) rows.map do |row| club = clubs[row.club_id] secs = row.total_secs.to_i hours = secs / 3600.0 sessions_count = row.sessions_count.to_i share = total_secs.positive? ? secs.to_f / total_secs : 0.0 allocated_cents = (total_cost_cents * share).round revenue_cents = revenue_by_club[row.club_id].to_i storage_bytes = storage_by_club[row.club_id].to_i { club_id: row.club_id, club_name: row.club_name, plan_slug: club&.subscription&.plan&.slug, sessions: sessions_count, hours: hours.round(2), hours_share_pct: (share * 100).round(1), allocated_cost_cents: allocated_cents, revenue_cents: revenue_cents, margin_cents: revenue_cents - allocated_cents, cost_per_hour_cents: hours.positive? ? (allocated_cents / hours).round : nil, cost_per_session_cents: sessions_count.positive? ? (allocated_cents / sessions_count) : nil, storage_bytes: storage_bytes } end.sort_by { |row| [-row[:hours], row[:club_name]] } end private def build_period(month) range = month_range(month) cost_cents = PlatformCostEntry.for_month(month).sum(:amount_cents) sessions_scope = ended_sessions.where(ended_at: range) total_secs = sessions_scope.sum(:total_duration_secs).to_i hours = total_secs / 3600.0 sessions = sessions_scope.count clubs_active = distinct_active_clubs(range) revenue_cents = Billing::Payment.where(status: "paid", paid_at: range).sum(:amount_cents) storage_bytes = Recording.ready.sum(:byte_size).to_i storage_gb = storage_bytes.positive? ? storage_bytes / (1024.0**3) : 0.0 kpis = compute_kpis( cost_cents: cost_cents, hours: hours, sessions: sessions, clubs_active: clubs_active, revenue_cents: revenue_cents, storage_gb: storage_gb ) { month: month, cost_cents: cost_cents, sessions: sessions, hours: hours.round(2), clubs_active: clubs_active, revenue_cents: revenue_cents, storage_bytes: storage_bytes, **kpis } end def compute_kpis(cost_cents:, hours:, sessions:, clubs_active:, revenue_cents:, storage_gb:) margin_cents = revenue_cents.to_i - cost_cents.to_i margin_pct = revenue_cents.to_i.positive? ? ((margin_cents.to_f / revenue_cents.to_i) * 100).round(1) : nil hours_f = hours.to_f sessions_i = sessions.to_i clubs_i = clubs_active.to_i storage_f = storage_gb.to_f cost_i = cost_cents.to_i { cost_per_hour_cents: hours_f.positive? ? (cost_i / hours_f).round : nil, cost_per_session_cents: sessions_i.positive? ? (cost_i / sessions_i) : nil, cost_per_club_cents: clubs_i.positive? ? (cost_i / clubs_i) : nil, revenue_per_hour_cents: hours_f.positive? ? (revenue_cents.to_i / hours_f).round : nil, cost_per_gb_cents: storage_f.positive? ? (cost_i / storage_f).round : nil, margin_cents: margin_cents, margin_pct: margin_pct } end def month_range(month) month.beginning_of_month.beginning_of_day..month.end_of_month.end_of_day end def ended_sessions StreamSession.where(status: "ended") end def distinct_active_clubs(range) ended_sessions .where(ended_at: range) .joins(match: { team: :club }) .distinct .count("clubs.id") end def session_rows(range) ended_sessions .where(ended_at: range) .joins(match: { team: :club }) .group("clubs.id", "clubs.name") .select( "clubs.id AS club_id", "clubs.name AS club_name", "COUNT(stream_sessions.id) AS sessions_count", "SUM(stream_sessions.total_duration_secs) AS total_secs" ) end def revenue_by_club(range) Billing::Payment.where(status: "paid", paid_at: range).group(:club_id).sum(:amount_cents) end def storage_by_club_index Recording.ready.joins(:team).group("teams.club_id").sum(:byte_size) end end end