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>
This commit is contained in:
2026-06-02 22:26:19 +02:00
parent 566104aff4
commit 82ff1972a0
24 changed files with 388 additions and 7 deletions

View File

@@ -0,0 +1,101 @@
module Billing
class AdminCompedSubscription
class Error < StandardError; end
VALID_PLANS = %w[premium_light premium_full].freeze
def self.grant(club:, plan_slug:, reason:, admin:)
new(club: club, plan_slug: plan_slug, reason: reason, admin: admin).grant
end
def self.revoke(club:, admin:)
new(club: club, admin: admin).revoke
end
def initialize(club:, plan_slug: nil, reason: nil, admin: nil)
@club = club
@plan_slug = plan_slug.to_s.presence
@reason = reason.to_s.strip.presence
@admin = admin
end
def grant
raise Error, "Piano non valido" unless @plan_slug.in?(VALID_PLANS)
release_stripe_schedule!
AssignPlan.call(
club: @club,
plan_slug: @plan_slug,
status: "active",
stripe_attrs: comped_attrs.merge(
pending_plan_id: nil,
pending_billing_interval: nil,
stripe_schedule_id: nil,
cancel_at_period_end: false
)
)
end
def revoke
sub = @club.subscription
raise Error, "Nessun abbonamento omaggio attivo" unless sub&.admin_comped?
if sub.stripe_subscription_id.present? && MatchLiveTv.stripe_enabled?
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)
if stripe_sub.status.in?(%w[active trialing past_due])
Billing::Stripe::CheckoutSync.from_subscription(stripe_sub, club: @club)
clear_comped_flags!(sub.reload)
return sub
end
end
AssignPlan.call(
club: @club,
plan_slug: "free",
status: "active",
stripe_attrs: {
admin_comped: false,
admin_comped_reason: nil,
admin_comped_at: nil,
admin_comped_by_id: nil,
pending_plan_id: nil,
pending_billing_interval: nil,
stripe_schedule_id: nil
}
)
end
private
def comped_attrs
{
admin_comped: true,
admin_comped_reason: @reason,
admin_comped_at: Time.current,
admin_comped_by_id: @admin&.id
}
end
def clear_comped_flags!(sub)
sub.update!(
admin_comped: false,
admin_comped_reason: nil,
admin_comped_at: nil,
admin_comped_by_id: nil
)
end
def release_stripe_schedule!
sub = @club.subscription
return if sub.blank? || sub.stripe_schedule_id.blank?
return unless MatchLiveTv.stripe_enabled?
::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
end
end

View File

@@ -18,6 +18,7 @@ module Billing
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)

View File

@@ -70,6 +70,8 @@ module Billing
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)

View File

@@ -14,6 +14,7 @@ module Billing
sub = @club.subscription
return false unless sub&.stripe_subscription_id.present?
return false if sub.free?
return false if sub.admin_comped?
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)