module Billing module Stripe class ChangePlan VALID_PLANS = CheckoutSession::VALID_PLANS.freeze def self.call(club:, plan_slug:, interval: nil) new(club: club, plan_slug: plan_slug, interval: interval).call end def initialize(club:, plan_slug:, interval: nil) @club = club @plan_slug = plan_slug.to_s @interval = interval @sub = club.subscription end def call raise "Stripe non configurato" unless MatchLiveTv.stripe_enabled? raise "Piano non valido" unless VALID_PLANS.include?(@plan_slug) raise "Abbonamento omaggio: contatta il supporto per cambiare piano a pagamento" if @sub&.admin_comped? raise "Nessun abbonamento Stripe attivo" if @sub.blank? || @sub.stripe_subscription_id.blank? interval = resolve_interval(@sub) if @sub.plan.slug == @plan_slug && @sub.billing_interval == interval && !@sub.pending_plan_id? raise "Sei già su questo piano e intervallo di fatturazione" end kind = PlanChangeKind.classify( from_plan_slug: @sub.plan.slug, from_interval: @sub.billing_interval, to_plan_slug: @plan_slug, to_interval: interval ) case kind when :upgrade perform_upgrade!(interval: interval) when :downgrade ScheduleDowngrade.call(club: @club, plan_slug: @plan_slug, interval: interval) else raise "Sei già su questo piano e intervallo di fatturazione" end end private def perform_upgrade!(interval:) release_schedule_if_any! price_id = PriceCatalog.price_id(plan_slug: @plan_slug, interval: interval) stripe_sub = ::Stripe::Subscription.retrieve(@sub.stripe_subscription_id) item_id = stripe_sub.items.data.first.id updated = ::Stripe::Subscription.update( stripe_sub.id, items: [{ id: item_id, price: price_id }], metadata: { club_id: @club.id, plan_slug: @plan_slug, billing_interval: interval }, proration_behavior: "always_invoice", payment_behavior: "error_if_incomplete" ) invoice = pay_open_subscription_invoice!(updated.id) apply_local_plan!(updated, interval: interval) ChangePlanResult.new( kind: :upgrade, plan_slug: @plan_slug, interval: interval, effective_at: Time.current, amount_cents: invoice&.amount_paid, currency: invoice&.currency, invoice_id: invoice&.id ) end def release_schedule_if_any! return if @sub.stripe_schedule_id.blank? ::Stripe::SubscriptionSchedule.release(@sub.stripe_schedule_id) rescue ::Stripe::InvalidRequestError nil ensure @sub.update!(stripe_schedule_id: nil, pending_plan_id: nil, pending_billing_interval: nil) end def pay_open_subscription_invoice!(stripe_sub_id) invoice = ::Stripe::Invoice.list(subscription: stripe_sub_id, status: "open", limit: 1).data.first return nil unless invoice paid = invoice.status == "paid" ? invoice : ::Stripe::Invoice.pay(invoice.id) RecordPayment.call(stripe_invoice: paid, club: @club) if paid.status == "paid" paid rescue ::Stripe::CardError => e raise ::Stripe::CardError, "Pagamento non riuscito: #{e.message}" end def resolve_interval(sub) return PriceCatalog.normalize_interval(@interval) if @interval.present? sub.billing_interval.presence || PriceCatalog::DEFAULT_INTERVAL end def apply_local_plan!(stripe_sub, interval:) period_start, period_end = SubscriptionPeriod.times(stripe_sub) Billing::AssignPlan.call( club: @club, plan_slug: @plan_slug, status: map_status(stripe_sub.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: interval, pending_plan_id: nil, pending_billing_interval: nil, stripe_schedule_id: nil } ) 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