Date: 2016-Nov-17
Presentation by: Sebastián Sogamoso
Meetup: Dealing with Payments
Video: Dealing with Payments – Video
AlicanTech
Hola
My name is Sebastián Sogamoso
Tweet to me @sebasoga
!
DEALING WITH
PAYMENTS
This talk
is about
This talk is about
Trust
Colleagues
Company
Users
Trust
Information is safe
App works as expected
Trust
Search
Photo uploads
Chat
Comments
Trust
Payments
Trust
Common pitfalls
Payment gateway
2XX status code doesn’t necessarily mean the transaction was successful
class Transfer
def create(user, trip)
@response = PaymentGatewayClient.create_transfer(
amount: trip.price,
currency: user.currency,
destination: user.account,
description: trip.to_s
)
end
def successful?
@response.status_code == “201”
end
end
class Transfer
def create(user, trip)
@response = PaymentGatewayClient.create_transfer(
amount: trip.price,
currency: user.currency,
destination: user.account,
description: trip.to_s
)
end
def successful?
@response.status_code == “201”
end
end
class Transfer
def create(user, trip)
@response = PaymentGatewayClient.create_transfer(
amount: trip.price,
currency: user.currency,
destination: user.account,
description: trip.to_s
)
end
def successful?
@response.status_code == “201”
end
end
class Transfer
def create(user, trip)
@response = PaymentGatewayClient.create_transfer(
amount: trip.price,
currency: user.currency,
destination: user.account,
description: trip.to_s
)
end
def successful?
@response.status_code == “201”
end
end
user = User.last
trip = User.trips.last
transfer = Transfer.new.create(user, trip)
class Transfer
def create(user, trip)
@response = PaymentGatewayClient.create_transfer(
amount: trip.price,
currency: user.currency,
destination: user.account,
description: trip.to_s
)
end
def successful?
@response.status_code == “201”
end
end
user = User.last
trip = User.trips.last
transfer = Transfer.new.create(user, trip)
transfer.successful?
# Not correct
Payment gateway
Your system
Create transfer
Payment gateway
Your system
Create transfer
Ok (200)
Payment gateway
Your system
Create transfer
Ok (200)
Transfer result
Payment gateway
Your system
Create transfer
Ok (200)
Transfer result
Ok
Operations should be idempotent
# Not idempotent
def charge_not_idempotent(invoice)
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
# Idempotent
def charge_idempotent(invoice)
if invoice.charged?
Stripe::Charge.retrieve(invoice.charge_id)
else
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
end
# Not idempotent
def charge_not_idempotent(invoice)
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
# Idempotent
def charge_idempotent(invoice)
if invoice.charged?
Stripe::Charge.retrieve(invoice.charge_id)
else
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
end
# Not idempotent
def charge_not_idempotent(invoice)
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
# Idempotent
def charge_idempotent(invoice)
if invoice.charged?
Stripe::Charge.retrieve(invoice.charge_id)
else
Stripe::Charge.create(
amount: invoice.total,
currency: invoice.currency,
source: invoice.user.credit_card,
description: invoice.to_s
)
end
end
invoice = Invoice.last
# It will charge the invoice 10 times
10.times do
charge_not_idempotent(invoice)
end
# It will charge the invoice 1 time
10.times do
charge_idempotent(invoice)
end
Tracking history
How did this record get in its current state?
What was this record’s state at some point in the past?
• Hard to know which code led a state change
• Previous data might be overwritten
• Relevant code might not exist
Record events every time state changes
Never update them
DB features or libraries
gem ‘paper_trail’
class Charge < ActiveRecord::Base
belongs_to :user
has_many :events, class_name: "ChargeEvent"
end
class Charge < ActiveRecord::Base
belongs_to :user
has_many :events, class_name: "ChargeEvent"
end
class Charge < ActiveRecord::Base
belongs_to :user
has_many :events, class_name: "ChargeEvent"
end
class ChargeEvent < ActiveRecord::Base
EVENT_TYPES = %w(creation success failure refund)
belongs_to :charge
validates :charge, :data, presence: true
validates :event_type, inclusion: { in: EVENT_TYPES}
end
class Charge < ActiveRecord::Base
belongs_to :user
has_many :events, class_name: "ChargeEvent"
end
class ChargeEvent < ActiveRecord::Base
EVENT_TYPES = %w(creation success failure refund)
belongs_to :charge
validates :charge, :data, presence: true
validates :event_type, inclusion: { in: EVENT_TYPES}
end
class Order < ActiveRecord::Base
attr_reader :user, :items, :total
def charge
ActiveRecord.transation do
charge = Charge.create!(user: user, total: total)
charge.events.create!(data: data, event_type: "creation")
self.update!(charge: charge)
end
end
private
def data
{items: items.inspect, total: total, user: user}
end
end
class Order < ActiveRecord::Base
attr_reader :user, :items, :total
def charge
ActiveRecord.transation do
charge = Charge.create!(user: user, total: total)
charge.events.create!(data: data, event_type: "creation")
self.update!(charge: charge)
end
end
private
def data
{items: items.inspect, total: total, user: user}
end
end
class Order < ActiveRecord::Base
attr_reader :user, :items, :total
def charge
ActiveRecord.transation do
charge = Charge.create!(user: user, total: total)
charge.events.create!(data: data, event_type: "creation")
self.update!(charge: charge)
end
end
private
def data
{items: items.inspect, total: total, user: user}
end
end
class PaymentGateway
def initialize(client)
@client = client
end
def charge(order)
charge
= order.charge
response
= @client.create_charge(order.charge)
transaction_id = response.transaction_id
if response.successful?
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "success")
else
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "failure")
end
end
end
class PaymentGateway
def initialize(client)
@client = client
end
def charge(order)
charge
= order.charge
response
= @client.create_charge(order.charge)
transaction_id = response.transaction_id
if response.successful?
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "success")
else
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "failure")
end
end
end
class PaymentGateway
def initialize(client)
@client = client
end
def charge(order)
charge
= order.charge
response
= @client.create_charge(order.charge)
transaction_id = response.transaction_id
if response.successful?
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "success")
else
charge.update!(transaction_id: response.transaction_id)
charge.events.create!(data: transaction_id, event_type: "failure")
end
end
end
Recap
Payment gateway
Tracking history
Idempotent operations
Buy a leg of jamón Iberico online
$
$$$$$$
$$$$$$
Trust
Payments are a huge part of the User Experience
5 things to provide a good payments UX
Async transactions
Payment status visibility
Deleting payment information
Prepare to ship big changes
Dealing with bugs
Async transactions
class ChargesController < ApplicationController
def create
@charge = current_user.charges.build.new(charge_params)
if charge_user(@charge)
redirect_to :charge_sucess
else
render :cart
end
end
...
class ChargesController < ApplicationController
def create
@charge = current_user.charges.build.new(charge_params)
if charge_user(@charge)
redirect_to :charge_sucess
else
render :cart
end
end
...
def charge_user(charge)
result = Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
charge.status = result.status
charge.save
result.status == "succeeded"
end
def charge_user(charge)
result = Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
charge.status = result.status
charge.save
result.status == "succeeded"
end
def charge_user(charge)
result = Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
charge.status = result.status
charge.save
result.status == "succeeded"
end
What seems to be happening
Your system
User
Your system
Payment gateway
User
Your system
Payment gateway
User
What really happens
Your system
Payment gateway
User
Your system
Payment Processor
Payment gateway
User
Your system
Payment Processor
Payment gateway
User
Card’s
Brand
Your system
Payment Processor
Card’s
Brand
Payment gateway
User
Card Issuing Bank
Your system
Payment Processor
Card’s
Brand
Payment gateway
User
Card Issuing Bank
Marchant’s
Bank
1.000.000 per minute
Asynchronous implementation
class ChargesController < ApplicationController
def create
@charge = Charge.create(charge_params)
ChargeWorker.perform_async(@charge.id)
redirect_to :charge_in_process
end
end
class ChargesController < ApplicationController
def create
@charge = Charge.create(charge_params)
ChargeWorker.perform_async(@charge.id)
redirect_to :charge_in_process
end
end
Retries
class ChargeWorker
def perform(charge_id)
charge = ::Charge.find(charge_id)
Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
end
end
class ChargeWorker
include Sidekiq::Worker
def perform(charge_id)
charge = ::Charge.find(charge_id)
Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
end
end
class ChargeWorker
include Sidekiq::Worker
def perform(charge_id)
charge = ::Charge.find(charge_id)
Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
end
end
class ChargeWorker
include Sidekiq::Worker
sidekiq_options retry: 3
def perform(charge_id)
charge = ::Charge.find(charge_id)
Stripe::Charge.create(
amount: charge.amount,
currency: charge.currency,
source: charge.credit_card_token,
description: charge.product_name
)
end
end
Payment status visibility
Why don’t I see the refund in my card statement?
When am I getting payed out?
Why was I charged X amount?
Deleting payment information
Give users full control of their payment information
class CreditCardsController < ApplicationController
def destroy
@credit_card = current_user.credit_cards.find(params[:id])
@credit_card.destroy
redirect_to credit_cards_url, notice: "Credit card deleted"
end
end
end
class CreditCardsController < ApplicationController
def destroy
@credit_card = current_user.credit_cards.find(params[:id])
@credit_card.destroy
redirect_to credit_cards_url, notice: "Credit card deleted"
end
end
end
Settle payments first
class CreditCardsController < ApplicationController
def destroy
@credit_card = current_user.credit_cards.find(params[:id])
if current_user.charge_pending_trips
@credit_card.destroy
redirect_to credit_cards_url, notice: "Credit card deleted"
else
redirect_to @credit_card, notice: “Settlement failed"
end
end
end
class CreditCardsController < ApplicationController
def destroy
@credit_card = current_user.credit_cards.find(params[:id])
if current_user.charge_pending_trips
@credit_card.destroy
redirect_to credit_cards_url, notice: "Credit card deleted"
else
redirect_to @credit_card, notice: "Settlement failed"
end
end
end
class CreditCardsController < ApplicationController
def destroy
@credit_card = current_user.credit_cards.find(params[:id])
if current_user.charge_pending_trips
@credit_card.destroy
redirect_to credit_cards_url, notice: "Credit card deleted"
else
redirect_to @credit_card, notice: "Settlement failed"
end
end
end
Prepare to ship big changes
Discuss big changes
RFCs
Make changing stuff easy
Business logic
Payment gateway
Business logic
Payment gateway
Business logic
Payment gateway
Payment gateway one
Payment gateway two
Business logic
This is how it looks in code
class Charge
def initialize(payment_gateway)
@payment_gateway = payment_gateway
end
def create(user:, amount:)
@payment_gateway.new(user)
@payment_gateway.charge(amount)
end
end
class PaymentGatewayOne
def initialize(customer)
@customer = customer
end
def charge
...
end
end
class PaymentGatewayTwo
def initialize(user)
@user = user
end
def charge
...
end
end
> Charge.new(PaymentGatewayOne).charge(User.find(1), 100)
=> true
> Charge.new(PaymentGatewayOne).charge(User.find(1), 100)
=> true
> Charge.new(PaymentGatewayTwo).charge(User.find(1), 100)
=> true
> Charge.new(PaymentGatewayOne).charge(User.find(1), 100)
=> true
> Charge.new(PaymentGatewayTwo).charge(User.find(1), 100)
=> true
Simulate
class SimulationPaymentGateway
def initialize(user)
@user = user
end
def charge
Rails.logger.debug(“Charging $#{amount} to user ##{@user.id}”)
end
end
class SimulationPaymentGateway
def initialize(user)
@user = user
end
def charge
Rails.logger.debug(“Charging $#{amount} to user ##{@user.id}”)
end
end
> Charge.new(SimulationPaymentGateway).charge(User.find(1), 100)
> Charge.new(SimulationPaymentGateway).charge(User.find(1), 100)
Charging $100 to user #1
=> nil
Business logic
Payment gateway
Payment gateway
Simulation gateway
Business logic
Business logic
Simulation gateway
Dealing with bugs
Yes, you will introduce bugs
Catch them quick
How?
Monitoring & alerting
if:
charge_amount_greater_than_1000_dollars
then:
send_pager_duty_alert
if:
user_charged_more_than_once_for_same_trip
then:
send_pager_duty_alert
Test failure conditions
Have a damage control plan
1. Communication
Be proactive.
Reach out to affected users
External communication
Write a postmortem
Internal communication
2. Handbook to deal with 💩
Anyone can deal with payment bugs
• Refunds
• Reversals
• Recalculating accounts balance
3. Write documentation
• Structure
• Business rules
4. Screw up in favour of the customer
So…
Async transactions
Payment status visibility
Deleting payment information
Prepare to ship big changes
Dealing with bugs
The whole purpose
is to…
✔
Trust
Gracias
@sebasoga