Delegation

Wai-Yin and I were thinking about implementing Stripe Webhooks in one of our projects to help us keep our records in sync when users preformed actions in the Stripe dashboard (e.g. deleting a credit card or refunding a charge).

Stripe Webhooks offers a choice of adding a new endpoint for each type of event you want to handle or sending all events to a single endpoint. We decided to go with a single endpoint. To make that happen it would be nice if there were someone who could take care of any kind of event, maybe StripeEventManager could do it?

This will involve handling multiple message types through the same interface using delegation and custom event classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module Api
  module Admins
    class StripeHooksController < ActionController::Base
      def create
        event_manager = StripeEventManager.new stripe_payload
        event_manager.process_event
        render event_manager.response_body, status: event_manager.response_code
      end

      private

      def stripe_payload
        JSON.parse(request.body.read)
      end
    end
  end
end

Jim Gay has an interesting rant about the modern misconception of delegation. TL;DR: what most people think of delegation is actually message forwarding. He goes on to explain that in true delegation self will always refer to the original message recipient:

[W]hen you send a message to an object, it has a notion of “self” where it can find attributes and other methods. When that object delegates to another, then any reference to “self” always refers to the original message recipient. Always.

Turns out we just need the fake kind of delegation that's provided by Active Support

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StripeEventManager
  attr_accessor :request
  delegate :process_event, :response_body, :response_code, to: :event_handler

  def initialize(request)
    self.request = request
  end

  private

  def event_handler
    @event_hander ||= events_map[request[:type]].to_s.constantize.new(request)
  end

  def events_map
    event_handlers.map { |klass| [klass.event_name, klass] }.to_h
  end

  def event_handlers
    StripeEvents.constants.select { |c| StripeEvents.const_get(c).is_a? Class }
  end
end

And heres an example of an event handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module StripeEvents
  class CardDeleted
    EVENT_NAME = 'customer.card.deleted'
    attr_accssor :request, :response_body, :response_code

    def self.event_name
      EVENT_NAME
    end

    def initialize(request)
      fail StandardError, 'bad Stripe request' unless valid_request
      self.request = request
    end

    def process_event
      card = CreditCard.find_by(token: cart_token)
      card.update(deleted_at: Time.zone.now) if card
      self.response_code = 200
    end

    private

    def valid_request
      request.try(:[], :data).try(:[], :object).try(:[], :id)
    end

    def cart_token
      request[:data][:object][:id]
    end
  end
end