Billing Stripe, link regia mobile e staff solo trasmissione.
Aggiunge fatturazione club, pagina regia condivisibile senza account, roster squadre e rimuove la modalità controller dall'app (v1.1.0). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
76
backend/app/services/billing/stripe/change_plan.rb
Normal file
76
backend/app/services/billing/stripe/change_plan.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
module Billing
|
||||
module Stripe
|
||||
class ChangePlan
|
||||
VALID_PLANS = CheckoutSession::VALID_PLANS.freeze
|
||||
|
||||
def self.call(club:, plan_slug:)
|
||||
new(club: club, plan_slug: plan_slug).call
|
||||
end
|
||||
|
||||
def initialize(club:, plan_slug:)
|
||||
@club = club
|
||||
@plan_slug = plan_slug.to_s
|
||||
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 "Sei già su questo piano" if sub.plan.slug == @plan_slug
|
||||
|
||||
price_id = Plan[@plan_slug].stripe_price_id.presence || stripe_price_id_for(@plan_slug)
|
||||
raise "Price ID Stripe mancante per #{@plan_slug}" if price_id.blank?
|
||||
|
||||
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 },
|
||||
proration_behavior: "create_prorations"
|
||||
)
|
||||
|
||||
apply_local_plan!(updated)
|
||||
updated
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stripe_price_id_for(slug)
|
||||
case slug
|
||||
when "premium_light" then MatchLiveTv.stripe_premium_light_price_id
|
||||
when "premium_full" then MatchLiveTv.stripe_premium_full_price_id
|
||||
end
|
||||
end
|
||||
|
||||
def apply_local_plan!(stripe_sub)
|
||||
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
|
||||
}
|
||||
)
|
||||
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
|
||||
@@ -64,11 +64,15 @@ module Billing
|
||||
end
|
||||
|
||||
def success_url
|
||||
"#{dashboard_url}?checkout=success"
|
||||
"#{billing_url}?checkout=success"
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
"#{dashboard_url}?checkout=canceled"
|
||||
"#{billing_url}?checkout=canceled"
|
||||
end
|
||||
|
||||
def billing_url
|
||||
"#{MatchLiveTv.app_public_url.chomp('/')}/clubs/#{@club.id}/billing"
|
||||
end
|
||||
|
||||
def dashboard_url
|
||||
|
||||
90
backend/app/services/billing/stripe/record_payment.rb
Normal file
90
backend/app/services/billing/stripe/record_payment.rb
Normal file
@@ -0,0 +1,90 @@
|
||||
module Billing
|
||||
module Stripe
|
||||
class RecordPayment
|
||||
def self.call(stripe_invoice:, club: nil)
|
||||
new(stripe_invoice: stripe_invoice, club: club).call
|
||||
end
|
||||
|
||||
def initialize(stripe_invoice:, club: nil)
|
||||
@stripe_invoice = stripe_invoice
|
||||
@club = club
|
||||
end
|
||||
|
||||
def call
|
||||
@club ||= resolve_club
|
||||
return unless @club
|
||||
|
||||
payment = Billing::Payment.find_or_initialize_by(stripe_invoice_id: @stripe_invoice.id)
|
||||
payment.assign_attributes(
|
||||
club: @club,
|
||||
stripe_payment_intent_id: payment_intent_id,
|
||||
stripe_charge_id: charge_id,
|
||||
amount_cents: @stripe_invoice.amount_paid,
|
||||
currency: @stripe_invoice.currency,
|
||||
status: map_status(@stripe_invoice.status),
|
||||
plan_slug: plan_slug,
|
||||
description: description,
|
||||
paid_at: paid_at,
|
||||
invoice_pdf_url: @stripe_invoice.invoice_pdf,
|
||||
receipt_url: @stripe_invoice.hosted_invoice_url
|
||||
)
|
||||
payment.save!
|
||||
payment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_club
|
||||
club_id = metadata_club_id(@stripe_invoice)
|
||||
return Club.find_by(id: club_id) if club_id.present?
|
||||
|
||||
sub_id = @stripe_invoice.subscription
|
||||
return unless sub_id.present?
|
||||
|
||||
Club.joins(:subscription).find_by(subscriptions: { stripe_subscription_id: sub_id })
|
||||
end
|
||||
|
||||
def metadata_club_id(obj)
|
||||
meta = obj.metadata
|
||||
return nil if meta.blank?
|
||||
|
||||
meta["club_id"] || meta[:club_id]
|
||||
end
|
||||
|
||||
def plan_slug
|
||||
meta = @stripe_invoice.subscription_details&.metadata || @stripe_invoice.metadata
|
||||
meta["plan_slug"] || meta[:plan_slug]
|
||||
end
|
||||
|
||||
def description
|
||||
lines = @stripe_invoice.lines&.data
|
||||
return @stripe_invoice.description if lines.blank?
|
||||
|
||||
lines.first.description.presence || @stripe_invoice.description
|
||||
end
|
||||
|
||||
def payment_intent_id
|
||||
pi = @stripe_invoice.payment_intent
|
||||
pi.is_a?(String) ? pi : pi&.id
|
||||
end
|
||||
|
||||
def charge_id
|
||||
charge = @stripe_invoice.charge
|
||||
charge.is_a?(String) ? charge : charge&.id
|
||||
end
|
||||
|
||||
def paid_at
|
||||
ts = @stripe_invoice.status_transitions&.paid_at
|
||||
ts ? Time.zone.at(ts) : Time.current
|
||||
end
|
||||
|
||||
def map_status(stripe_status)
|
||||
case stripe_status
|
||||
when "paid" then "paid"
|
||||
when "uncollectible", "void" then "failed"
|
||||
else "failed"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
backend/app/services/billing/stripe/subscription_period.rb
Normal file
27
backend/app/services/billing/stripe/subscription_period.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
module Billing
|
||||
module Stripe
|
||||
module SubscriptionPeriod
|
||||
module_function
|
||||
|
||||
def times(stripe_sub)
|
||||
item = stripe_sub.items&.data&.first
|
||||
start_at = read_value(stripe_sub, :current_period_start) || read_value(item, :current_period_start)
|
||||
end_at = read_value(stripe_sub, :current_period_end) || read_value(item, :current_period_end)
|
||||
[unix_time(start_at), unix_time(end_at)]
|
||||
end
|
||||
|
||||
def read_value(object, field)
|
||||
return nil if object.blank?
|
||||
return nil unless object.respond_to?(field)
|
||||
|
||||
object.public_send(field)
|
||||
end
|
||||
|
||||
def unix_time(value)
|
||||
return nil if value.blank?
|
||||
|
||||
Time.zone.at(value.to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
36
backend/app/services/billing/stripe/sync_payments.rb
Normal file
36
backend/app/services/billing/stripe/sync_payments.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
module Billing
|
||||
module Stripe
|
||||
class SyncPayments
|
||||
def self.call(club:)
|
||||
new(club: club).call
|
||||
end
|
||||
|
||||
def initialize(club:)
|
||||
@club = club
|
||||
end
|
||||
|
||||
def call
|
||||
customer_id = @club.subscription&.stripe_customer_id
|
||||
return 0 if customer_id.blank?
|
||||
|
||||
count = 0
|
||||
starting_after = nil
|
||||
|
||||
loop do
|
||||
list = ::Stripe::Invoice.list(customer: customer_id, limit: 25, starting_after: starting_after)
|
||||
list.data.each do |invoice|
|
||||
next unless invoice.status == "paid"
|
||||
|
||||
RecordPayment.call(stripe_invoice: invoice, club: @club)
|
||||
count += 1
|
||||
end
|
||||
break unless list.has_more
|
||||
|
||||
starting_after = list.data.last.id
|
||||
end
|
||||
|
||||
count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,6 +19,8 @@ module Billing
|
||||
downgrade_to_free(@event.data.object)
|
||||
when "invoice.payment_failed"
|
||||
mark_past_due(@event.data.object)
|
||||
when "invoice.paid"
|
||||
record_payment(@event.data.object)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,8 +51,7 @@ module Billing
|
||||
plan_slug = "premium_full" if plan_slug == "premium"
|
||||
plan = Plan[plan_slug]
|
||||
status = map_status(stripe_sub.status)
|
||||
period_start = unix_time(stripe_sub.current_period_start)
|
||||
period_end = unix_time(stripe_sub.current_period_end)
|
||||
period_start, period_end = SubscriptionPeriod.times(stripe_sub)
|
||||
|
||||
Billing::AssignPlan.call(
|
||||
club: club,
|
||||
@@ -84,6 +85,10 @@ module Billing
|
||||
)
|
||||
end
|
||||
|
||||
def record_payment(invoice)
|
||||
Billing::Stripe::RecordPayment.call(stripe_invoice: invoice)
|
||||
end
|
||||
|
||||
def mark_past_due(invoice)
|
||||
return if invoice.subscription.blank?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user