Refactor credit purchasing (#13955)

* Refactor credit purchasing

* Add comment about legacy API

* Re-arrange steps
This commit is contained in:
Michael Kohl 2021-06-15 09:04:51 +07:00 committed by GitHub
parent aba6033d9b
commit 974266a610
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 84 deletions

View file

@ -23,94 +23,20 @@ class CreditsController < ApplicationController
def create
not_authorized if params[:organization_id].present? && !current_user.org_admin?(params[:organization_id])
@number_to_purchase = params[:credit][:number_to_purchase].to_i
number_to_purchase = params[:credit][:number_to_purchase].to_i
return unless make_payment
credits_attributes = Array.new(@number_to_purchase) do
# unfortunately Rails requires the timestamps to be present and doesn't add them automatically
# see <https://github.com/rails/rails/issues/35493>
now = Time.current
attrs = { created_at: now, updated_at: now, cost: cost_per_credit / 100.0 }
if params[:organization_id].present?
@purchaser = Organization.find(params[:organization_id])
attrs[:organization_id] = params[:organization_id]
else
@purchaser = current_user
attrs[:user_id] = current_user.id
end
attrs
end
Credit.insert_all(credits_attributes)
@purchaser.credits_count = @purchaser.credits.size
@purchaser.spent_credits_count = @purchaser.credits.spent.size
@purchaser.unspent_credits_count = @purchaser.credits.unspent.size
@purchaser.save
redirect_to credits_path, notice: "#{@number_to_purchase} new credits purchased!"
end
private
def make_payment
find_or_create_customer
find_or_create_card
update_user_stripe_info
create_charge
true
rescue Payments::PaymentsError => e
flash[:error] = e.message
redirect_to purchase_credits_path
false
end
def find_or_create_customer
@customer = if current_user.stripe_id_code
Payments::Customer.get(current_user.stripe_id_code)
else
Payments::Customer.create(email: current_user.email)
end
end
def find_or_create_card
@card = if params[:stripe_token]
Payments::Customer.create_source(@customer.id, params[:stripe_token])
else
Payments::Customer.get_source(@customer, params[:selected_card])
end
end
def update_user_stripe_info
current_user.update_column(:stripe_id_code, @customer.id) if current_user.stripe_id_code.nil?
end
def create_charge
Payments::Customer.charge(
customer: @customer,
amount: generate_cost,
description: "Purchase of #{@number_to_purchase} credits.",
card_id: @card&.id,
payment = Payments::ProcessCreditPurchase.call(
current_user,
number_to_purchase,
purchase_options: params.slice(:stripe_token, :selected_card, :organization_id),
)
end
def generate_cost
@number_to_purchase * cost_per_credit
end
def cost_per_credit
prices = Settings::General.credit_prices_in_cents
case @number_to_purchase
when ..9
prices[:small]
when 10..99
prices[:medium]
when 100..999
prices[:large]
if payment.success?
@purchaser = payment.purchaser
redirect_to credits_path, notice: "#{number_to_purchase} new credits purchased!"
else
prices[:xlarge]
flash[:error] = payment.error
redirect_to purchase_credits_path
end
end
end

View file

@ -0,0 +1,124 @@
module Payments
# This service encapsulates purchasing credits via the Stripe API.
#
# NOTE: We still use the legacy checkout API here. While this doesn't
# support all features of Stripe's new API (e.g. SCA) it's well suited
# for our use-case of occasional one-off purchases.
# An exploration of alternatives and the decision to stick to the legacy API
# can be found here: https://github.com/forem/internalEngineering/issues/364
class ProcessCreditPurchase
def self.call(user, credits_count, purchase_options:)
new(
user,
credits_count,
purchase_options: purchase_options,
).call
end
attr_reader :purchaser, :error
def initialize(user, credits_count, purchase_options:)
self.user = user
self.credits_count = credits_count
self.purchase_options = purchase_options
self.success = false
end
def call
process_purchase
create_credits if success?
self
end
def success?
success
end
private
attr_accessor :user, :credits_count, :success, :purchase_options
attr_writer :purchaser, :error
def process_purchase
customer = find_or_create_customer
update_user_stripe_info(customer)
card = find_or_create_card(customer)
create_charge(customer, card)
self.success = true
rescue Payments::PaymentsError => e
self.error = e.message
end
def find_or_create_customer
if user.stripe_id_code
Payments::Customer.get(user.stripe_id_code)
else
Payments::Customer.create(email: user.email)
end
end
def find_or_create_card(customer)
if purchase_options[:stripe_token]
Payments::Customer.create_source(customer.id, purchase_options[:stripe_token])
else
Payments::Customer.get_source(customer, purchase_options[:selected_card])
end
end
def update_user_stripe_info(customer)
user.update_column(:stripe_id_code, customer.id) if user.stripe_id_code.nil?
end
def create_charge(customer, card)
Payments::Customer.charge(
customer: customer,
amount: credits_count * cost_per_credit,
description: "Purchase of #{credits_count} credits.",
card_id: card&.id,
)
end
def create_credits
purchaser = user
credits_attributes = Array.new(credits_count) do
# unfortunately Rails requires the timestamps to be present and doesn't add them automatically
# see <https://github.com/rails/rails/issues/35493>
now = Time.current
attrs = { created_at: now, updated_at: now, cost: cost_per_credit / 100.0 }
if purchase_options[:organization_id].present?
purchaser = Organization.find(purchase_options[:organization_id])
attrs[:organization_id] = purchase_options[:organization_id]
else
attrs[:user_id] = user.id
end
attrs
end
Credit.insert_all(credits_attributes)
purchaser.update(
credits_count: purchaser.credits.size,
spent_credits_count: purchaser.credits.spent.size,
unspent_credits_count: purchaser.credits.unspent.size,
)
self.purchaser = purchaser
end
def cost_per_credit
prices = Settings::General.credit_prices_in_cents
case credits_count
when ..9
prices[:small]
when 10..99
prices[:medium]
when 100..999
prices[:large]
else
prices[:xlarge]
end
end
end
end