module Billing module Stripe # Allinea DB locale con Stripe dopo Checkout o webhook (idempotente). class CheckoutSync def self.call(club:, session_id: nil) new(club: club, session_id: session_id).call end def self.from_session(session) new.apply_from_session(session) end def self.from_subscription(stripe_sub, club: nil, plan_slug: nil, billing_interval: nil) new.apply_from_subscription(stripe_sub, club: club, plan_slug: plan_slug, billing_interval: billing_interval) end def initialize(club: nil, session_id: nil) @club = club @session_id = session_id end def call return false unless MatchLiveTv.stripe_enabled? if @session_id.present? session = ::Stripe::Checkout::Session.retrieve(@session_id) return false unless session_paid?(session) apply_from_session(session) return true end sync_from_customer end def apply_from_session(session) club = resolve_club(session) || @club return unless club sub = club.subscription || club.build_subscription sub.update!(stripe_customer_id: session.customer) if session.customer.present? return unless session.subscription.present? stripe_sub = ::Stripe::Subscription.retrieve(session.subscription) plan_slug = metadata_plan_slug(session) || "premium_light" plan_slug = "premium_full" if plan_slug == "premium" apply_from_subscription( stripe_sub, club: club, plan_slug: plan_slug, billing_interval: metadata_billing_interval(session) ) end def apply_from_subscription(stripe_sub, club: nil, plan_slug: nil, billing_interval: nil) club ||= Club.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: stripe_sub.id }) club ||= resolve_club(stripe_sub) return unless club plan_slug ||= metadata_plan_slug(stripe_sub) || infer_plan_slug_from_price(stripe_sub) || "premium_light" plan_slug = "premium_full" if plan_slug == "premium" plan = Plan[plan_slug] status = map_status(stripe_sub.status) period_start, period_end = SubscriptionPeriod.times(stripe_sub) billing_interval = billing_interval.presence || metadata_billing_interval(stripe_sub) || infer_interval_from_price(stripe_sub) || PriceCatalog::DEFAULT_INTERVAL sub = club.subscription return sub if sub&.admin_comped? clear_pending = sub&.pending_plan_id.present? && (sub.pending_plan.slug == plan.slug || infer_plan_slug_from_price(stripe_sub) == sub.pending_plan.slug) Billing::AssignPlan.call( club: club, plan_slug: plan.slug, status: status, stripe_attrs: { stripe_customer_id: stripe_sub.customer, stripe_subscription_id: stripe_sub.id, current_period_start: period_start, current_period_end: period_end, cancel_at_period_end: stripe_sub.cancel_at_period_end, billing_interval: billing_interval, pending_plan_id: clear_pending ? nil : sub&.pending_plan_id, pending_billing_interval: clear_pending ? nil : sub&.pending_billing_interval, stripe_schedule_id: clear_pending ? nil : sub&.stripe_schedule_id } ) end private def sync_from_customer return false unless @club customer_id = @club.subscription&.stripe_customer_id return false if customer_id.blank? stripe_sub = latest_active_subscription_for_club(customer_id, @club.id) return false unless stripe_sub apply_from_subscription(stripe_sub, club: @club) true end def latest_active_subscription_for_club(customer_id, club_id) list = ::Stripe::Subscription.list(customer: customer_id, status: "all", limit: 20) list.data .select { |s| %w[active trialing].include?(s.status) } .select { |s| subscription_belongs_to_club?(s, club_id) } .max_by(&:created) end def subscription_belongs_to_club?(stripe_sub, club_id) meta_club = metadata_club_id(stripe_sub) return meta_club == club_id if meta_club.present? team_id = metadata_team_id(stripe_sub) return Team.find_by(id: team_id)&.club_id == club_id if team_id.present? false end def session_paid?(session) session.payment_status == "paid" || session.status == "complete" end def infer_plan_slug_from_price(stripe_sub) price_id = stripe_sub.items&.data&.first&.price&.id return "premium_light" if price_id == MatchLiveTv.stripe_premium_light_monthly_price_id return "premium_light" if price_id == MatchLiveTv.stripe_premium_light_yearly_price_id return "premium_full" if price_id == MatchLiveTv.stripe_premium_full_monthly_price_id return "premium_full" if price_id == MatchLiveTv.stripe_premium_full_yearly_price_id "premium_light" end def infer_interval_from_price(stripe_sub) interval = stripe_sub.items&.data&.first&.price&.recurring&.interval return "monthly" if interval == "month" return "yearly" if interval == "year" nil end def resolve_club(obj) club_id = metadata_club_id(obj) return Club.find_by(id: club_id) if club_id.present? team_id = metadata_team_id(obj) return Team.find_by(id: team_id)&.club if team_id.present? @club end def metadata_billing_interval(obj) meta = obj.metadata return nil if meta.blank? val = meta["billing_interval"] || meta[:billing_interval] return nil if val.blank? PriceCatalog.normalize_interval(val) rescue ArgumentError nil end def metadata_plan_slug(obj) meta = obj.metadata return nil if meta.blank? meta["plan_slug"] || meta[:plan_slug] end def metadata_club_id(obj) meta = obj.metadata return nil if meta.blank? meta["club_id"] || meta[:club_id] end def metadata_team_id(obj) meta = obj.metadata return nil if meta.blank? meta["team_id"] || meta[:team_id] end def map_status(stripe_status) case stripe_status when "trialing" then "trialing" when "active" then "active" when "past_due", "unpaid" then "past_due" when "canceled", "incomplete_expired" then "canceled" else "incomplete" end end end end end