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) end end private def handle_checkout_completed(session) team_id = metadata_team_id(session) return if team_id.blank? team = Team.find_by(id: team_id) return unless team sub = team.subscription || team.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, team: team, plan_slug: plan_slug) end end def sync_subscription(stripe_sub, team: nil, plan_slug: nil) team ||= Team.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: stripe_sub.id }) team ||= Team.find_by(id: metadata_team_id(stripe_sub)) return unless team 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 = unix_time(stripe_sub.current_period_start) period_end = unix_time(stripe_sub.current_period_end) Billing::AssignPlan.call( team: team, 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) team = Team.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: stripe_sub.id }) return unless team Billing::AssignPlan.call( team: team, 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 mark_past_due(invoice) return if invoice.subscription.blank? team = Team.joins(:subscription) .find_by(subscriptions: { stripe_subscription_id: invoice.subscription }) return unless team&.subscription team.subscription.update!(status: "past_due") 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_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