Skip to content

Commit

Permalink
Merge pull request #45 from spaghetticode/payment-intents-single-payment
Browse files Browse the repository at this point in the history
Create a single charge when using Stripe Payment Intents
  • Loading branch information
spaghetticode authored Apr 10, 2020
2 parents c5d153f + 891e31a commit e68d896
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ SolidusStripe.PaymentIntents.prototype.onIntentsPayment = function(payment) {
'Content-Type': 'application/json'
},
body: JSON.stringify({
form_data: this.form.serialize(),
spree_payment_method_id: this.config.id,
stripe_payment_method_id: payment.paymentMethod.id,
authenticity_token: this.authToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
form_data: this.form.serialize(),
spree_payment_method_id: this.config.id,
stripe_payment_intent_id: result.paymentIntent.id,
authenticity_token: this.authToken
Expand Down
49 changes: 29 additions & 20 deletions app/controllers/solidus_stripe/intents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,19 @@ class IntentsController < Spree::BaseController

def confirm
begin
if params[:stripe_payment_method_id].present?
intent = stripe.create_intent(
(current_order.total * 100).to_i,
params[:stripe_payment_method_id],
currency: current_order.currency,
confirmation_method: 'manual',
confirm: true,
setup_future_usage: 'on_session',
metadata: { order_id: current_order.id }
)
elsif params[:stripe_payment_intent_id].present?
intent = stripe.confirm_intent(params[:stripe_payment_intent_id], nil)
@intent = begin
if params[:stripe_payment_method_id].present?
create_intent
elsif params[:stripe_payment_intent_id].present?
stripe.confirm_intent(params[:stripe_payment_intent_id], nil)
end
end
rescue Stripe::CardError => e
render json: { error: e.message }, status: 500
return
end

generate_payment_response(intent)
generate_payment_response
end

private
Expand All @@ -33,20 +27,35 @@ def stripe
@stripe ||= Spree::PaymentMethod::StripeCreditCard.find(params[:spree_payment_method_id])
end

def generate_payment_response(intent)
response = intent.params
def generate_payment_response
response = @intent.params
# Note that if your API version is before 2019-02-11, 'requires_action'
# appears as 'requires_source_action'.
if %w[requires_source_action requires_action].include?(response['status']) && response['next_action']['type'] == 'use_stripe_sdk'
render json: {
requires_action: true,
stripe_payment_intent_client_secret: response['client_secret']
}
elsif response['status'] == 'succeeded'
render json: {
requires_action: true,
stripe_payment_intent_client_secret: response['client_secret']
}
elsif response['status'] == 'requires_capture'
SolidusStripe::CreateIntentsOrderService.new(@intent, stripe, self).call
render json: { success: true }
else
render json: { error: response['error']['message'] }, status: 500
end
end

def create_intent
stripe.create_intent(
(current_order.total * 100).to_i,
params[:stripe_payment_method_id],
description: "Solidus Order ID: #{current_order.number} (pending)",
currency: current_order.currency,
confirmation_method: 'manual',
capture_method: 'manual',
confirm: true,
setup_future_usage: 'off_session',
metadata: { order_id: current_order.id }
)
end
end
end
39 changes: 39 additions & 0 deletions app/decorators/models/spree/order_update_attributes_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Spree
module OrderUpdateAttributesDecorator
def assign_payments_attributes
return if payments_attributes.empty?
return if adding_new_stripe_payment_intents_card?

stripe_intents_pending_payments.each(&:void_transaction!)

super
end

private

def adding_new_stripe_payment_intents_card?
paying_with_stripe_intents? && stripe_intents_pending_payments.any?
end

def stripe_intents_pending_payments
@stripe_intents_pending_payments ||= order.payments.valid.select do |payment|
payment_method = payment.payment_method
payment.pending? && stripe_intents?(payment_method)
end
end

def paying_with_stripe_intents?
if id = payments_attributes.first&.dig(:payment_method_id)
stripe_intents?(Spree::PaymentMethod.find(id))
end
end

def stripe_intents?(payment_method)
payment_method.respond_to?(:v3_intents?) && payment_method.v3_intents?
end

::Spree::OrderUpdateAttributes.prepend(self)
end
end
11 changes: 11 additions & 0 deletions app/decorators/models/spree/payment_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Spree
module PaymentDecorator
def gateway_order_identifier
gateway_order_id
end

::Spree::Payment.prepend(self)
end
end
70 changes: 70 additions & 0 deletions app/models/solidus_stripe/create_intents_order_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module SolidusStripe
class CreateIntentsOrderService
attr_reader :intent, :stripe, :controller

delegate :request, :current_order, :params, to: :controller

def initialize(intent, stripe, controller)
@intent, @stripe, @controller = intent, stripe, controller
end

def call
invalidate_previous_payment_intents_payments
payment = create_payment
description = "Solidus Order ID: #{payment.gateway_order_identifier}"
stripe.update_intent(nil, response['id'], nil, description: description)
end

private

def invalidate_previous_payment_intents_payments
if stripe.v3_intents?
current_order.payments.pending.where(payment_method: stripe).each(&:void_transaction!)
end
end

def create_payment
Spree::OrderUpdateAttributes.new(
current_order,
payment_params,
request_env: request.headers.env
).apply

Spree::Payment.find_by(response_code: response['id']).tap do |payment|
payment.update!(state: :pending)
end
end

def payment_params
card = response['charges']['data'][0]['payment_method_details']['card']
address_attributes = form_data['payment_source'][stripe.id.to_s]['address_attributes']

{
payments_attributes: [{
payment_method_id: stripe.id,
amount: current_order.total,
response_code: response['id'],
source_attributes: {
month: card['exp_month'],
year: card['exp_year'],
cc_type: card['brand'],
gateway_payment_profile_id: response['payment_method'],
last_digits: card['last4'],
name: current_order.bill_address.full_name,
address_attributes: address_attributes
}
}]
}
end

def response
intent.params
end

def form_data
Rack::Utils.parse_nested_query(params[:form_data])
end
end
end
11 changes: 3 additions & 8 deletions app/models/spree/payment_method/stripe_credit_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class StripeCreditCard < Spree::PaymentMethod::CreditCard
'Visa' => 'visa'
}

delegate :create_intent, :update_intent, :confirm_intent, to: :gateway

def stripe_config(order)
{
id: id,
Expand Down Expand Up @@ -59,14 +61,6 @@ def payment_profiles_supported?
true
end

def create_intent(*args)
gateway.create_intent(*args)
end

def confirm_intent(*args)
gateway.confirm_intent(*args)
end

def purchase(money, creditcard, transaction_options)
gateway.purchase(*options_for_purchase_or_auth(money, creditcard, transaction_options))
end
Expand Down Expand Up @@ -142,6 +136,7 @@ def options_for_purchase_or_auth(money, creditcard, transaction_options)
options = {}
options[:description] = "Solidus Order ID: #{transaction_options[:order_id]}"
options[:currency] = transaction_options[:currency]
options[:off_session] = true if v3_intents?

if customer = creditcard.gateway_customer_profile_id
options[:customer] = customer
Expand Down
Loading

0 comments on commit e68d896

Please sign in to comment.