Files
MatchLiveTv/backend/app/services/billing/stripe/change_plan.rb
Emiliano Frascaro 82ff1972a0 Aggiunge abbonamenti omaggio admin per sponsor e promozioni.
Gli admin possono concedere o revocare Premium Light/Full senza Stripe;
il piano omaggio ha priorità sui webhook e la società vede un messaggio dedicato.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 22:26:19 +02:00

136 lines
4.6 KiB
Ruby

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