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:
11
backend/app/services/teams/entitlement_error.rb
Normal file
11
backend/app/services/teams/entitlement_error.rb
Normal 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
|
||||
239
backend/app/services/teams/entitlements.rb
Normal file
239
backend/app/services/teams/entitlements.rb
Normal 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
|
||||
Reference in New Issue
Block a user