Refactor credit purchasing (#13955)
* Refactor credit purchasing * Add comment about legacy API * Re-arrange steps
This commit is contained in:
parent
aba6033d9b
commit
974266a610
2 changed files with 134 additions and 84 deletions
|
|
@ -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
|
||||
|
|
|
|||
124
app/services/payments/process_credit_purchase.rb
Normal file
124
app/services/payments/process_credit_purchase.rb
Normal 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
|
||||
Loading…
Add table
Reference in a new issue