app/controllers/alaveteli_pro/stripe_webhooks_controller.rb
# Does not inherit from AlaveteliPro::BaseController because it doesn't need to
class AlaveteliPro::StripeWebhooksController < ApplicationController
class MissingTypeStripeWebhookError < StandardError; end
class UnknownPlanStripeWebhookError < StandardError; end
skip_before_action :html_response
rescue_from JSON::ParserError, MissingTypeStripeWebhookError do |exception|
# Invalid payload, reject the webhook
notify_exception(exception)
render json: { error: exception.message }, status: 400
end
rescue_from Stripe::SignatureVerificationError do |exception|
# Invalid signature, reject the webhook
notify_exception(exception)
render json: { error: exception.message }, status: 401
end
rescue_from UnknownPlanStripeWebhookError do |_exception|
# accept it so it doesn't get resent but don't process it
# (and don't generate an exception email for it)
render json: { message: 'Does not appear to be one of our plans' },
status: 200
end
before_action :read_event_notification, :check_for_event_type, :filter_hooks
def receive
case @stripe_event.type
when 'customer.subscription.deleted'
customer_subscription_deleted
when 'invoice.payment_succeeded'
invoice_payment_succeeded
when 'invoice.payment_failed'
invoice_payment_failed
else
store_unhandled_webhook
end
# send a 200 ok to acknowledge receipt of the webhook
# https://stripe.com/docs/webhooks#responding-to-a-webhook
render json: { message: 'OK' }, status: 200
end
private
def customer_subscription_deleted
account = pro_account_from_stripe_event(@stripe_event)
account.user.remove_role(:pro) if account
end
def invoice_payment_succeeded
charge_id = @stripe_event.data.object.charge
if charge_id
charge = Stripe::Charge.retrieve(charge_id)
subscription_id = @stripe_event.data.object.subscription
subscription = Stripe::Subscription.retrieve(subscription_id)
plan_name = subscription.plan.name
charge.description =
"#{ pro_site_name }: #{ plan_name }"
charge.save
end
end
def invoice_payment_failed
account = pro_account_from_stripe_event(@stripe_event)
if account
AlaveteliPro::SubscriptionMailer.payment_failed(account.user).deliver_now
end
end
def store_unhandled_webhook
Webhook.create(params: @stripe_event.to_h)
end
def read_event_notification
payload = request.body.read
sig_header = request.headers['HTTP_STRIPE_SIGNATURE']
endpoint_secret = AlaveteliConfiguration.stripe_webhook_secret
@stripe_event = nil
@stripe_event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
end
def pro_account_from_stripe_event(event)
customer_id = event.data.object.customer
ProAccount.find_by(stripe_customer_id: customer_id)
end
def check_for_event_type
unless @stripe_event.respond_to?(:type)
msg = "undefined method `type' for #{ @stripe_event.inspect }"
raise MissingTypeStripeWebhookError, msg
end
end
def notify_exception(error)
return unless send_exception_notifications?
ExceptionNotifier.notify_exception(error, env: request.env)
end
# ignore any that don't match our plan namespace
def filter_hooks
plans = []
case @stripe_event.data.object.object
when 'subscription'
plans = plan_ids(@stripe_event.data.object.items)
when 'invoice'
plans = plan_ids(@stripe_event.data.object.lines)
end
# ignore any plans that don't start with our namespace
plans.delete_if { |plan| !plan_matches_namespace?(plan) }
raise UnknownPlanStripeWebhookError if plans.empty?
end
def plan_matches_namespace?(plan_id)
(AlaveteliConfiguration.stripe_namespace == '' ||
plan_id =~ /^#{AlaveteliConfiguration.stripe_namespace}/)
end
def plan_ids(items)
items.map { |item| item.plan.id if item.plan }.compact.uniq
end
end