Billing upgrade/downgrade, inviti in app e diretta YouTube da mobile.

Upgrade Stripe con addebito immediato; downgrade programmato a fine periodo.
UX billing più sobria con box info; icone piani. API inviti e OAuth YouTube
per staff trasmissione; deep link join; scelta partita programmata o nuova.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 22:15:27 +02:00
parent 2cb267f991
commit 566104aff4
49 changed files with 1615 additions and 173 deletions

View File

@@ -0,0 +1,80 @@
module Billing
module Stripe
class ScheduleDowngrade
def self.call(club:, plan_slug:, interval:)
new(club: club, plan_slug: plan_slug, interval: interval).call
end
def initialize(club:, plan_slug:, interval:)
@club = club
@plan_slug = plan_slug.to_s
@interval = PriceCatalog.normalize_interval(interval)
@sub = club.subscription
end
def call
release_existing_schedule!
stripe_sub = ::Stripe::Subscription.retrieve(@sub.stripe_subscription_id)
period_start, period_end = SubscriptionPeriod.times(stripe_sub)
raise "Periodo di fatturazione non disponibile" if period_end.blank?
current_item = stripe_sub.items.data.first
new_price_id = PriceCatalog.price_id(plan_slug: @plan_slug, interval: @interval)
schedule = ::Stripe::SubscriptionSchedule.create(from_subscription: stripe_sub.id)
::Stripe::SubscriptionSchedule.update(
schedule.id,
{
end_behavior: "release",
phases: [
{
items: [{ price: current_item.price.id, quantity: 1 }],
start_date: schedule_phase_start(stripe_sub, period_start),
end_date: period_end.to_i
},
{
items: [{ price: new_price_id, quantity: 1 }],
metadata: {
club_id: @club.id,
plan_slug: @plan_slug,
billing_interval: @interval
}
}
]
}
)
pending_plan = Plan[@plan_slug]
@sub.update!(
pending_plan: pending_plan,
pending_billing_interval: @interval,
stripe_schedule_id: schedule.id
)
ChangePlanResult.new(
kind: :downgrade,
plan_slug: @plan_slug,
interval: @interval,
effective_at: period_end
)
end
private
def release_existing_schedule!
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 schedule_phase_start(stripe_sub, period_start)
period_start&.to_i || stripe_sub.items.data.first.current_period_start
end
end
end
end