Dealing with Payments

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

Jeroen Derks

Author: Jeroen Derks

Jeroen is the founder of the Alicante Tech meetup group. His current day job is to mostly build all kinds of applications, ranging from IoT to educational to corporate.

Leave a Reply