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 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

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