Separa heatmap mobile/desktop, opt-out analytics per operatori, dashboard costi con KPI e trend mensili. Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
5.0 KiB
Ruby
151 lines
5.0 KiB
Ruby
# 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
|