Initial commit: monorepo Match Live TV.

Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
module Teams
class EntitlementError < StandardError
attr_reader :code, :billing_url
def initialize(message, code: "premium_required", billing_url: nil)
super(message)
@code = code
@billing_url = billing_url
end
end
end

View File

@@ -0,0 +1,239 @@
module Teams
class Entitlements
ACTIVE_STREAM_STATUSES = %w[idle connecting live reconnecting paused].freeze
def initialize(team)
@team = team
end
def subscription
@subscription ||= @team.subscription || ensure_free_subscription!
end
def plan
subscription.plan
end
def active?
subscription.active?
end
def premium_active?
subscription.premium?
end
def premium_full?
subscription.premium_full?
end
def youtube_enabled?
active? && plan.youtube_enabled?
end
def phone_download_enabled?
active? && plan.phone_download_enabled?
end
def max_staff_transmission
plan.max_staff_transmission
end
def max_staff_regia
plan.max_staff_regia
end
def max_staff
plan.max_staff
end
def staff_count
staff_count_for("transmission") + staff_count_for("regia")
end
def staff_count_for(kind)
members_count_for(kind) + pending_invitations_count_for(kind)
end
def members_count
@team.user_teams.where(role: "member").count
end
def members_count_for(kind)
@team.user_teams.where(role: "member", staff_kind: kind).count
end
def pending_invitations_count
@team.team_invitations.pending.count
end
def pending_invitations_count_for(kind)
@team.team_invitations.pending.where(staff_kind: kind).count
end
def concurrent_streams_limit
plan.concurrent_streams_limit
end
def concurrent_streams_used
StreamSession.joins(:match)
.where(matches: { team_id: @team.id })
.where(status: ACTIVE_STREAM_STATUSES)
.count
end
def concurrent_streams_limit_reached?
limit = concurrent_streams_limit
return false if limit.nil?
concurrent_streams_used >= limit
end
def can_stream_on?(platform)
return false unless active?
platform = platform.to_s
return false unless plan.allows_platform?(platform)
true
end
def assert_can_stream_on!(platform)
platform = platform.to_s
unless active?
raise EntitlementError.new(
"Abbonamento non attivo per questa squadra",
code: "subscription_inactive",
billing_url: billing_url
)
end
if platform == "youtube" && !youtube_enabled?
raise EntitlementError.new(
"YouTube richiede un piano Premium",
code: "premium_required",
billing_url: billing_url
)
end
if platform == "youtube" && premium_full? == false && plan.youtube_mode != "matchlivetv_light"
raise EntitlementError.new(
"Il canale YouTube della società richiede Premium Full",
code: "premium_full_required",
billing_url: billing_url
)
end
return if plan.allows_platform?(platform)
raise EntitlementError.new("Piattaforma non consentita", code: "platform_denied", billing_url: billing_url)
end
def assert_concurrent_stream!(excluding_session: nil)
return unless active?
used = concurrent_streams_used
used -= 1 if excluding_session && ACTIVE_STREAM_STATUSES.include?(excluding_session.status)
limit = concurrent_streams_limit
return if limit.nil?
return if used < limit
raise EntitlementError.new(
"Limite dirette concorrenti raggiunto (#{limit}). Chiudi una diretta o passa a un piano superiore.",
code: "concurrent_limit_reached",
billing_url: billing_url
)
end
def assert_can_invite!(staff_kind: "transmission")
return unless active?
kind = staff_kind.to_s
unless TeamInvitation::STAFF_KINDS.include?(kind)
raise EntitlementError.new("Ruolo staff non valido", code: "invalid_staff_kind")
end
limit = staff_limit_for(kind)
return if limit.nil?
used = staff_count_for(kind)
return if used < limit
label = kind == "regia" ? "regia" : "trasmissione"
raise EntitlementError.new(
"Limite staff #{label} raggiunto (#{limit} all'anno). Passa a un piano superiore.",
code: "staff_limit_reached",
billing_url: billing_url
)
end
def staff_limit_for(kind)
kind == "regia" ? max_staff_regia : max_staff_transmission
end
def assert_can_connect_youtube!
unless premium_full?
raise EntitlementError.new(
"Collegamento al canale YouTube della società richiede Premium Full",
code: "premium_full_required",
billing_url: billing_url
)
end
assert_can_stream_on!("youtube")
end
def can_access_recordings?
active? && plan.recordings_enabled?
end
def recording_enabled_for_mediamtx?
can_access_recordings?
end
def recording_retention_days
plan.recording_days
end
def billing_url
"#{MatchLiveTv.app_public_url.chomp('/')}/teams/#{@team.id}/billing"
end
def staff_manage_url
"#{MatchLiveTv.app_public_url.chomp('/')}/teams/#{@team.id}/dashboard"
end
def as_json
{
plan_slug: plan.slug,
plan_name: plan.name,
premium_active: premium_active?,
premium_full: premium_full?,
subscription_status: subscription.status,
features: plan.features,
billing_url: billing_url,
staff_manage_url: staff_manage_url,
max_staff: max_staff,
max_staff_transmission: max_staff_transmission,
max_staff_regia: max_staff_regia,
staff_used: staff_count,
staff_transmission_used: staff_count_for("transmission"),
staff_regia_used: staff_count_for("regia"),
concurrent_streams_used: concurrent_streams_used,
concurrent_streams_limit: concurrent_streams_limit,
recordings_enabled: can_access_recordings?,
recording_retention_days: recording_retention_days,
phone_download_enabled: phone_download_enabled?,
youtube_enabled: youtube_enabled?,
youtube_mode: plan.youtube_mode
}
end
private
def ensure_free_subscription!
Billing::AssignPlan.call(team: @team, plan_slug: "free")
@team.reload.subscription
end
end
end