module Billing module Stripe class WebhookHandler def self.call(event) new(event).call end def initialize(event) @event = event end def call case @event.type when "checkout.session.completed" handle_checkout_completed(@event.data.object) when "customer.subscription.updated", "customer.subscription.created" sync_subscription(@event.data.object) when "customer.subscription.deleted" 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 private def handle_checkout_completed(session) club = resolve_club(session) return unless club sub = club.subscription || club.build_subscription sub.update!(stripe_customer_id: session.customer) if session.customer.present? if session.subscription.present? stripe_sub = ::Stripe::Subscription.retrieve(session.subscription) plan_slug = metadata_plan_slug(session) || "premium_light" plan_slug = "premium_full" if plan_slug == "premium" sync_subscription(stripe_sub, club: club, plan_slug: plan_slug) end end def sync_subscription(stripe_sub, club: nil, plan_slug: nil) club ||= Club.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: stripe_sub.id }) club ||= resolve_club(stripe_sub) return unless club plan_slug ||= metadata_plan_slug(stripe_sub) || "premium_light" plan_slug = "premium_full" if plan_slug == "premium" plan = Plan[plan_slug] status = map_status(stripe_sub.status) period_start, period_end = SubscriptionPeriod.times(stripe_sub) Billing::AssignPlan.call( club: club, plan_slug: plan.slug, status: 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 downgrade_to_free(stripe_sub) club = Club.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: stripe_sub.id }) return unless club Billing::AssignPlan.call( club: club, plan_slug: "free", status: "active", stripe_attrs: { stripe_subscription_id: nil, current_period_start: nil, current_period_end: nil, cancel_at_period_end: false } ) end def record_payment(invoice) Billing::Stripe::RecordPayment.call(stripe_invoice: invoice) end def mark_past_due(invoice) return if invoice.subscription.blank? club = Club.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: invoice.subscription }) return unless club&.subscription club.subscription.update!(status: "past_due") end def resolve_club(obj) club_id = metadata_club_id(obj) return Club.find_by(id: club_id) if club_id.present? team_id = metadata_team_id(obj) return Team.find_by(id: team_id)&.club if team_id.present? nil end def unix_time(value) return nil if value.blank? Time.zone.at(value.to_i) end def metadata_plan_slug(obj) meta = obj.metadata return nil if meta.blank? meta["plan_slug"] || meta[:plan_slug] || (meta.respond_to?(:plan_slug) ? meta.plan_slug : nil) end def metadata_club_id(obj) meta = obj.metadata return nil if meta.blank? meta["club_id"] || meta[:club_id] || (meta.respond_to?(:club_id) ? meta.club_id : nil) end def metadata_team_id(obj) meta = obj.metadata return nil if meta.blank? meta["team_id"] || meta[:team_id] || (meta.respond_to?(:team_id) ? meta.team_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