Skip to content

Instantly share code, notes, and snippets.

@joeljunstrom
Last active May 10, 2020 10:43
Show Gist options
  • Save joeljunstrom/96f212ce7240cd70d65affa32d60417f to your computer and use it in GitHub Desktop.
Save joeljunstrom/96f212ce7240cd70d65affa32d60417f to your computer and use it in GitHub Desktop.
class Payment < ApplicationRecord
end
class PaymentEventMessagesController < ApplicationController
# The controller does as little as possible. Not ethat this should probably verify that the payload is legit somehow
# We offload the heavy lifting to a job to we answer the gateway asap and not bog down our servers
def create
message = PaymentEventMessage.create!(gateway: gateway, payload: params[:payload])
ProcessPaymentEventMessageJob.perform_later(message.id)
head :ok
end
def gateway
raise ArgumentError, "unknown gateway #{params[:gateway]}" unless PaymentGateways.supported?(params[:gateway]j)
params[:gateway]
end
end
# The job can be responsible for tying togheter the pieces and call the correct objects
# You can also ofc push more work further down
class ProcessPaymentEventJob < ApplicationJob
def perform(payment_event_message_id)
message = PaymentEventMessage.find_by(id: payment_event_message_id)
return unless message
return if message.processed?
return if message.failed?
gateway = PaymentGateways.resolve(message.gateway)
event = gateway.interpret_message(message.payload)
payment = Payment.find(event.payment_id)
Payments.receive_event(payment, event)
rescue Payments::UnknownEventError => e
MyErrorHandlerService.report(e)
event.failed!
end
end
# A gateway is expected to return an object which signature is known and can
# be handled in a similar fashion regardless of which gateway was used.
# The job here is to convert foreign data structures to something that is owned by our system.
module PaymentGateways
GATEWAYS = {stripe: StripeGateway}
def self.resolve(name)
GATEWAYS.fetch(name).new
end
NoopEvent = Class.new do
def type
:noop
end
end
BillingEvent = Struct.new(:payment_id, :amount, :type, :failure)
class StripeGateway
def interpret_message(message)
event = Stripe::Event.construct_from(message)
case event.type
when "order.payment_succeeded"
BillingEvent.new(event.metadata["order_id"], event.amount, :billing_success, nil)
when "order.payment_failed"
# …
else
NoopEvent.new
else
end
end
end
# Could recide in app/lib/payments or something
# Makes changes to your system using known types of objects
module Payments
class UnknownEventError < StandardError; end
def self.receive_event(payment, event)
case event.type
when :noop
return
when :billing_success
Payments::PaymentReceived.call(payment, event.amount)
when :billing_failed
Payments::PaymentFailed.call(payment, event.failure)
else
raise UnknownEventError, "#{event.type} cannot be handled by #{self.class}"
end
event.processed!
end
# This could be where the knowledge about how an event changes our system lies
# Alter the db, call out to other services, log stuff etc.
PaymentReceived -> (payment, amount) { … }
PaymentFailed -> (payment, reason) { … }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment