module Public class ClubsController < WebBaseController include BrandingAttachments before_action :require_login! before_action :set_club, only: %i[show edit update billing checkout portal youtube_connect youtube_disconnect] before_action :require_billing_profile_for_premium_checkout!, only: :checkout def new redirect_to public_club_path(current_user.primary_club) if current_user.primary_club end def create if current_user.owned_clubs.exists? redirect_to public_club_path(current_user.primary_club), alert: t("flash.clubs.already_registered") return end club = Club.new(club_params) attach_branding_logo(club) club.save! ClubMembership.create!(user: current_user, club: club, role: "owner") first_team_name = params.dig(:first_team, :name).presence || t("club.new.default_first_team_name") first_team = club.teams.create!( name: first_team_name, sport: club.sport ) desired_plan = params[:plan].presence_in(%w[free premium_light premium_full]) || "free" Billing::AssignPlan.call(club: club, plan_slug: "free") Teams::StaffAssignment.designate_club_owner!(team: first_team, user: current_user) if desired_plan.in?(%w[premium_light premium_full]) interval = Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL if MatchLiveTv.stripe_enabled? unless club.billing_profile_complete? redirect_to public_club_billing_profile_path(club, plan: desired_plan, interval: interval), alert: t("flash.clubs.complete_billing_first") return end redirect_to public_club_checkout_path(club, plan: desired_plan, interval: interval) else redirect_to public_club_billing_path(club), notice: t("flash.clubs.created_complete_subscription") end else redirect_to public_club_path(club), notice: t("flash.clubs.created") end rescue ActiveRecord::RecordInvalid => e flash.now[:alert] = e.record.errors.full_messages.join(", ") render :new, status: :unprocessable_entity end def show require_club_owner!(@club) apply_checkout_flash! @entitlements_team = @club.teams.first @entitlements = @entitlements_team&.entitlements @teams = @club.teams.order(:name) @replay_stats = Recordings::ClubStats.new(@club).call if @entitlements&.can_access_recordings? end def edit require_club_owner!(@club) @entitlements = @club.teams.first&.entitlements end def update require_club_owner!(@club) @entitlements = @club.teams.first!.entitlements @club.assign_attributes(club_params) attach_branding_logo(@club) attach_cover_image(@club, entitlements: @entitlements) @club.save! enqueue_cover_slate_generation(@club) redirect_to public_club_path(@club), notice: t("flash.clubs.updated") rescue ActiveRecord::RecordInvalid => e @entitlements = @club.teams.first&.entitlements flash.now[:alert] = e.record.errors.full_messages.join(", ") render :edit, status: :unprocessable_entity rescue Teams::EntitlementError => e @entitlements = @club.teams.first&.entitlements flash.now[:alert] = e.message render :edit, status: :unprocessable_entity end def billing require_club_owner!(@club) sync_checkout_return! finalize_subscription_if_due! @subscription = @club.subscription&.reload apply_checkout_flash! @team = @club.teams.first! @entitlements = @team.entitlements @plans = Plan.ordered @payments = @club.billing_payments.recent.includes(:invoice).limit(50) @quote = @club.active_billing_quote @pending_transfer = @club.billing_transfer_orders.awaiting_payment.first end def checkout require_club_owner!(@club) if @club.subscription&.admin_comped? redirect_to public_club_billing_path(@club), alert: t("flash.clubs.comped_change_denied") return end if @club.active_billing_quote.present? redirect_to public_club_billing_path(@club), alert: t("flash.club_billing.quote_checkout_denied") return end unless MatchLiveTv.stripe_enabled? redirect_to public_club_billing_path(@club), alert: t("flash.clubs.stripe_not_configured") return end plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) || "premium_light" interval = checkout_interval_param target_plan = Plan[plan_slug] sub = @club.subscription if sub&.stripe_subscription_id.present? && sub.active? && sub.plan.slug == plan_slug && sub.billing_interval == interval && !sub.plan_change_pending? redirect_to public_club_billing_path(@club), notice: t("flash.clubs.already_on_plan", plan: target_plan.name, price: Billing::Stripe::PriceCatalog.label(plan_slug: plan_slug, interval: interval)) return end if sub&.plan_change_pending? && sub.pending_plan&.slug == plan_slug && sub.pending_billing_interval == interval when_label = sub.current_period_end.present? ? I18n.l(sub.current_period_end, format: :long) : t("billing.messages.next_renewal") redirect_to public_club_billing_path(@club), notice: t("flash.clubs.change_already_scheduled", plan: target_plan.name, date: when_label) return end if sub&.stripe_subscription_id.present? && sub.active? result = Billing::Stripe::ChangePlan.call(club: @club, plan_slug: plan_slug, interval: interval) redirect_to public_club_billing_path(@club), notice: Billing::Stripe::PlanChangeMessages.flash_notice( result, current_plan: sub.plan ) else url = Billing::Stripe::CheckoutSession.new( club: @club, user: current_user, plan_slug: plan_slug, interval: interval ).url redirect_to url, allow_other_host: true end rescue ArgumentError => e redirect_to public_club_billing_path(@club), alert: e.message rescue ::Stripe::StripeError => e Rails.logger.warn("[Stripe checkout] #{e.message}") redirect_to public_club_billing_path(@club), alert: t("flash.clubs.stripe_error", message: e.message) end def youtube_connect require_club_owner!(@club) entitlements = @club.teams.first!.entitlements entitlements.assert_can_connect_youtube! if ENV["YOUTUBE_CLIENT_ID"].blank? redirect_to public_club_path(@club), alert: t("flash.clubs.youtube_oauth_not_configured") return end return_app = params[:return_app] == "1" state = Youtube::OauthState.for_club( club_id: @club.id, user_id: current_user.id, return_app: return_app ) redirect_to Youtube::OauthUrl.build(state: state), allow_other_host: true rescue Teams::EntitlementError => e redirect_to public_club_path(@club), alert: e.message end def youtube_disconnect require_club_owner!(@club) @club.youtube_credential&.destroy! redirect_to public_club_path(@club), notice: t("flash.clubs.youtube_disconnected") end def portal require_club_owner!(@club) unless MatchLiveTv.stripe_enabled? redirect_to public_club_billing_path(@club), alert: t("flash.clubs.portal_not_configured") return end url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user).portal_url redirect_to url, allow_other_host: true end private def set_club @club = current_user.owned_clubs.find(params[:id]) end def club_params p = params.require(:club).permit( :name, :sport, :logo_url, :primary_color, :secondary_color, :billing_entity_type, :billing_legal_name, :billing_vat_number, :billing_fiscal_code, :billing_email, :billing_phone, :billing_address_line, :billing_city, :billing_province, :billing_postal_code, :billing_country, :billing_recipient_code, :billing_pec ) p[:primary_color] = normalize_hex_color(p[:primary_color], "#e53935") p[:secondary_color] = normalize_hex_color(p[:secondary_color], "#ffffff") p[:sport] = Sports::Catalog.normalize_key(p[:sport]) if p[:sport].present? p end def require_billing_profile_for_premium_checkout! require_club_owner!(@club) plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) return if plan_slug.blank? return if @club.billing_profile_complete? redirect_to public_club_billing_profile_path( @club, plan: plan_slug, interval: params[:interval].presence ), alert: t("flash.clubs.complete_billing_first") end def checkout_interval_param Billing::Stripe::PriceCatalog.normalize_interval(params[:interval]) rescue ArgumentError Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL end def finalize_subscription_if_due! return unless MatchLiveTv.stripe_enabled? Billing::Stripe::FinalizeSubscription.call(club: @club) rescue ::Stripe::StripeError => e Rails.logger.warn("[Stripe finalize subscription] #{e.message}") end def sync_checkout_return! return unless params[:checkout] == "success" return unless MatchLiveTv.stripe_enabled? Billing::Stripe::CheckoutSync.call( club: @club, session_id: params[:session_id].presence ) rescue ::Stripe::StripeError => e Rails.logger.warn("[Stripe checkout sync] #{e.message}") end def apply_checkout_flash! case params[:checkout] when "success" plan_name = @subscription&.plan&.name || "Premium" flash.now[:notice] = t("flash.clubs.checkout_success", plan: plan_name) when "canceled" flash.now[:alert] = t("flash.clubs.checkout_canceled") end end end end