From 974266a610629a07626e9fb1211df6998f2df468 Mon Sep 17 00:00:00 2001 From: Michael Kohl Date: Tue, 15 Jun 2021 09:04:51 +0700 Subject: [PATCH] Refactor credit purchasing (#13955) * Refactor credit purchasing * Add comment about legacy API * Re-arrange steps --- app/controllers/credits_controller.rb | 94 ++----------- .../payments/process_credit_purchase.rb | 124 ++++++++++++++++++ 2 files changed, 134 insertions(+), 84 deletions(-) create mode 100644 app/services/payments/process_credit_purchase.rb diff --git a/app/controllers/credits_controller.rb b/app/controllers/credits_controller.rb index 23c9d59e5..b2f47df0d 100644 --- a/app/controllers/credits_controller.rb +++ b/app/controllers/credits_controller.rb @@ -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 - 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 diff --git a/app/services/payments/process_credit_purchase.rb b/app/services/payments/process_credit_purchase.rb new file mode 100644 index 000000000..f9cb3aa44 --- /dev/null +++ b/app/services/payments/process_credit_purchase.rb @@ -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 + 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