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:
@@ -11,37 +11,88 @@ module Billing
|
||||
@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)
|
||||
|
||||
sub = @club.subscription
|
||||
raise "Nessun abbonamento Stripe attivo" if sub.blank? || sub.stripe_subscription_id.blank?
|
||||
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
|
||||
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
|
||||
|
||||
price_id = PriceCatalog.price_id(plan_slug: @plan_slug, interval: interval)
|
||||
kind = PlanChangeKind.classify(
|
||||
from_plan_slug: @sub.plan.slug,
|
||||
from_interval: @sub.billing_interval,
|
||||
to_plan_slug: @plan_slug,
|
||||
to_interval: interval
|
||||
)
|
||||
|
||||
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)
|
||||
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: "create_prorations"
|
||||
proration_behavior: "always_invoice",
|
||||
payment_behavior: "error_if_incomplete"
|
||||
)
|
||||
|
||||
invoice = pay_open_subscription_invoice!(updated.id)
|
||||
apply_local_plan!(updated, interval: interval)
|
||||
updated
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
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?
|
||||
@@ -61,7 +112,10 @@ module Billing
|
||||
current_period_start: period_start,
|
||||
current_period_end: period_end,
|
||||
cancel_at_period_end: stripe_sub.cancel_at_period_end,
|
||||
billing_interval: interval
|
||||
billing_interval: interval,
|
||||
pending_plan_id: nil,
|
||||
pending_billing_interval: nil,
|
||||
stripe_schedule_id: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user