Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dealing with payments

Dealing with payments

When things go right and our product starts making money everyone is happy, but sometimes this means the start of the nightmare for people working with payments.

Let's not sugar coat it. In this talk you'll learn about some where thing went terribly wrong, some of them involved loosing money. Stories of stuff that can easily get overlooked, about the most common mistakes when working with payments and things you probably won't consider until shit hits the fan. All of this so that you don't run into the same problems and don't have to learn those lessons the hard way.

Presented at: Rails Pacific 2016

92d08794b535e41a4082c57ea547546e?s=128

Sebastian Sogamoso

May 19, 2016
Tweet

Transcript

  1. Dealing with Payments WHEN MAKING MONEY BECOMES A HEADACHE

  2. My name is Sebastián Sogamoso Tweet me @sebasoga

  3. None
  4. This talk is about

  5. Trust This talk is about

  6. Colleagues

  7. Company

  8. Users

  9. Trust

  10. Trust Information is safe

  11. Information is safe App works as expected Trust

  12. Information is safe App works as expected Trust

  13. Trust

  14. Trust

  15. Trust

  16. Geolocation Chat service

  17. Weather updates Traffic updates Reminders Trust

  18. Payments Trust

  19. Payments Trust

  20. Common pitfalls

  21. Payment gateway

  22. 2XX status code doesn’t necessarily mean the transaction was successful

  23. 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
  24. 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
  25. 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
  26. 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)
  27. 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
  28. Payment gateway Your system Create transfer

  29. Payment gateway Your system Ok (200) Create transfer

  30. Payment gateway Your system Ok (200) Transfer result Create transfer

  31. Payment gateway Your system Ok (200) Transfer result Ok Create

    transfer
  32. Operations should be idempotent

  33. # 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
  34. # 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
  35. # 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
  36. 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
  37. Tracking history

  38. How did this record get in its current state?

  39. What was this record’s state at some point in the

    past?
  40. • Hard to know which code led a state change

    • Previous data might be overwritten • Relevant code might not exist
  41. Record events every time state changes

  42. Never update them

  43. gem 'paper_trail'

  44. class Charge < ActiveRecord::Base belongs_to :user has_many :events, class_name: "ChargeEvent"

    end
  45. class Charge < ActiveRecord::Base belongs_to :user has_many :events, class_name: "ChargeEvent"

    end
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. Idempotent operations Payment gateway Tracking history

  55. Great news!

  56. 5

  57. ActionPayment

  58. ActionPayment ⚡TurboPayments⚡

  59. Trust

  60. Payments are a huge part of the User Experience

  61. 5 things to provide a good payments UX

  62. Async transactions

  63. Payment status visibility

  64. Deleting payment information

  65. Prepare to ship big changes

  66. Dealing with bugs

  67. Async transactions

  68. 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 ...
  69. 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 ...
  70. 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
  71. 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
  72. 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
  73. What seems to be happening

  74. Your system User

  75. Your system Payment gateway User

  76. Your system User Payment gateway

  77. What really happens

  78. Your system User Payment gateway

  79. Payment Processor Your system User Payment gateway

  80. Payment Processor Card’s Brand Your system User Payment gateway

  81. Payment Processor Card Issuing Bank Card’s Brand Your system User

    Payment gateway
  82. Payment Processor Card Issuing Bank Marchant’s Bank Card’s Brand Your

    system User Payment gateway
  83. Asynchronous implementation

  84. class ChargesController < ApplicationController def create @charge = Charge.create(charge_params) ChargeWorker.perform_async(@charge.id)

    redirect_to :charge_in_process end end
  85. class ChargesController < ApplicationController def create @charge = Charge.create(charge_params) ChargeWorker.perform_async(@charge.id)

    redirect_to :charge_in_process end end
  86. Retries

  87. 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
  88. 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
  89. 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
  90. 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
  91. Payment status visibility

  92. Why don’t I see the refund in my card statement?

  93. When am I getting payed out?

  94. Why was I charged X amount?

  95. None
  96. Deleting payment information

  97. Give users full control of their payment information

  98. None
  99. 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
  100. 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
  101. Settle payments first

  102. 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
  103. 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
  104. 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
  105. Prepare to ship big changes

  106. Discuss big changes

  107. RFCs

  108. None
  109. Make changing stuff easy

  110. Business logic Payment gateway

  111. Payment gateway Business logic

  112. Business logic Payment gateway

  113. Business logic Payment gateway two Payment gateway one

  114. This is how it looks in code

  115. 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
  116. class PaymentGatewayOne def initialize(customer) @customer = customer end def charge

    ... end end class PaymentGatewayTwo def initialize(user) @user = user end def charge ... end end
  117. > Charge.new(PaymentGatewayOne).charge(User.find(1), 100) => true

  118. > Charge.new(PaymentGatewayOne).charge(User.find(1), 100) => true > Charge.new(PaymentGatewayTwo).charge(User.find(1), 100) => true

  119. > Charge.new(PaymentGatewayOne).charge(User.find(1), 100) => true > Charge.new(PaymentGatewayTwo).charge(User.find(1), 100) => true

  120. Simulate

  121. class SimulationPaymentGateway def initialize(user) @user = user end def charge

    Rails.logger.debug(“Charging $#{amount} to user ##{@user.id}”) end end
  122. class SimulationPaymentGateway def initialize(user) @user = user end def charge

    Rails.logger.debug(“Charging $#{amount} to user ##{@user.id}”) end end
  123. > Charge.new(SimulationPaymentGateway).charge(User.find(1), 100)

  124. > Charge.new(SimulationPaymentGateway).charge(User.find(1), 100) Charging $100 to user #1 => nil

  125. Payment gateway Business logic

  126. Business logic Simulation gateway Payment gateway

  127. Simulation gateway Business logic

  128. Dealing with bugs

  129. Yes, you will introduce bugs

  130. Catch them quick

  131. How?

  132. Monitoring & alterting

  133. if: charge_amount_greater_than_1000_dollars then: send_pager_duty_alert

  134. if: user_charged_more_than_once_for_same_trip then: send_pager_duty_alert

  135. Have a damage control plan

  136. 1. Communication

  137. Be proactive. Reach out to affected users External communication

  138. Write a postmortem Internal communication

  139. None
  140. 2. Handbook to deal with

  141. Anyone can deal with payment bugs

  142. • Refunds • Reversals • Recalculating accounts balance

  143. 3. Write documentation

  144. • Structure • Business rules

  145. Async transactions Payment status visibility Deleting payment information Prepare to

    ship big changes Dealing with bugs
  146. The whole purpose is to…

  147. Trust ✔

  148. 拽拽 @sebasoga