mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #4757 from luisramos0/stripe_sca_extra
StripeSCA - Handle further SCA auth request when checking out
This commit is contained in:
@@ -35,6 +35,8 @@ class CheckoutController < Spree::StoreController
|
||||
rescue_from Spree::Core::GatewayError, with: :rescue_from_spree_gateway_error
|
||||
|
||||
def edit
|
||||
return handle_redirect_from_stripe if valid_payment_intent_provided?
|
||||
|
||||
# This is only required because of spree_paypal_express. If we implement
|
||||
# a version of paypal that uses this controller, and more specifically
|
||||
# the #update_failed method, then we can remove this call
|
||||
@@ -151,6 +153,23 @@ class CheckoutController < Spree::StoreController
|
||||
end
|
||||
end
|
||||
|
||||
def valid_payment_intent_provided?
|
||||
params["payment_intent"]&.starts_with?("pi_") &&
|
||||
@order.state == "payment" &&
|
||||
@order.payments.last.state == "pending" &&
|
||||
@order.payments.last.response_code == params["payment_intent"]
|
||||
end
|
||||
|
||||
def handle_redirect_from_stripe
|
||||
if advance_order_state(@order) && order_complete?
|
||||
checkout_succeeded
|
||||
redirect_to(order_path(@order)) && return
|
||||
else
|
||||
flash[:error] = order_workflow_error
|
||||
checkout_failed
|
||||
end
|
||||
end
|
||||
|
||||
def checkout_workflow(shipping_method_id)
|
||||
while @order.state != "complete"
|
||||
if @order.state == "payment"
|
||||
@@ -165,11 +184,12 @@ class CheckoutController < Spree::StoreController
|
||||
return update_failed
|
||||
end
|
||||
|
||||
update_result
|
||||
update_response
|
||||
end
|
||||
|
||||
def redirect_to_payment_gateway
|
||||
redirect_path = Checkout::PaymentRedirect.new(params).path
|
||||
redirect_path = Checkout::PaypalRedirect.new(params).path
|
||||
redirect_path = Checkout::StripeRedirect.new(params, @order).path if redirect_path.blank?
|
||||
return if redirect_path.blank?
|
||||
|
||||
render json: { path: redirect_path }, status: :ok
|
||||
@@ -193,27 +213,27 @@ class CheckoutController < Spree::StoreController
|
||||
end
|
||||
end
|
||||
|
||||
def update_result
|
||||
if @order.state == "complete" || @order.completed?
|
||||
save_order_addresses_as_user_default
|
||||
ResetOrderService.new(self, current_order).call
|
||||
|
||||
update_succeeded
|
||||
def update_response
|
||||
if order_complete?
|
||||
checkout_succeeded
|
||||
update_succeeded_response
|
||||
else
|
||||
update_failed
|
||||
end
|
||||
end
|
||||
|
||||
def save_order_addresses_as_user_default
|
||||
user_default_address_setter = UserDefaultAddressSetter.new(@order, spree_current_user)
|
||||
user_default_address_setter.set_default_bill_address if params[:order][:default_bill_address]
|
||||
user_default_address_setter.set_default_ship_address if params[:order][:default_ship_address]
|
||||
def order_complete?
|
||||
@order.state == "complete" || @order.completed?
|
||||
end
|
||||
|
||||
def update_succeeded
|
||||
def checkout_succeeded
|
||||
Checkout::PostCheckoutActions.new(@order).success(self, params, spree_current_user)
|
||||
|
||||
session[:access_token] = current_order.token
|
||||
flash[:notice] = t(:order_processed_successfully)
|
||||
end
|
||||
|
||||
def update_succeeded_response
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
respond_with(@order, location: order_path(@order))
|
||||
@@ -225,9 +245,15 @@ class CheckoutController < Spree::StoreController
|
||||
end
|
||||
|
||||
def update_failed
|
||||
current_order.updater.shipping_address_from_distributor
|
||||
RestartCheckout.new(@order).call
|
||||
checkout_failed
|
||||
update_failed_response
|
||||
end
|
||||
|
||||
def checkout_failed
|
||||
Checkout::PostCheckoutActions.new(@order).failure
|
||||
end
|
||||
|
||||
def update_failed_response
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render :edit
|
||||
|
||||
@@ -59,6 +59,7 @@ module Spree
|
||||
# It destroys the whole customer object
|
||||
def destroy_at_stripe
|
||||
stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id, {})
|
||||
|
||||
stripe_customer.delete if stripe_customer
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
require 'stripe/profile_storer'
|
||||
require 'stripe/credit_card_cloner'
|
||||
require 'stripe/authorize_response_patcher'
|
||||
require 'stripe/payment_intent_validator'
|
||||
require 'active_merchant/billing/gateways/stripe_payment_intents'
|
||||
require 'active_merchant/billing/gateways/stripe_decorator'
|
||||
|
||||
@@ -32,9 +34,26 @@ module Spree
|
||||
|
||||
# NOTE: the name of this method is determined by Spree::Payment::Processing
|
||||
def purchase(money, creditcard, gateway_options)
|
||||
provider.purchase(*options_for_purchase_or_auth(money, creditcard, gateway_options))
|
||||
begin
|
||||
payment_intent_id = fetch_payment_intent(creditcard, gateway_options)
|
||||
rescue Stripe::StripeError => e
|
||||
return failed_activemerchant_billing_response(e.message)
|
||||
end
|
||||
|
||||
options = basic_options(gateway_options)
|
||||
options[:customer] = creditcard.gateway_customer_profile_id
|
||||
provider.capture(money, payment_intent_id, options)
|
||||
rescue Stripe::StripeError => e
|
||||
failed_activemerchant_billing_response(e.message)
|
||||
end
|
||||
|
||||
# NOTE: the name of this method is determined by Spree::Payment::Processing
|
||||
def authorize(money, creditcard, gateway_options)
|
||||
authorize_response = provider.authorize(*options_for_authorize(money,
|
||||
creditcard,
|
||||
gateway_options))
|
||||
Stripe::AuthorizeResponsePatcher.new(authorize_response).call!
|
||||
rescue Stripe::StripeError => e
|
||||
# This will be an error caused by generating a stripe token
|
||||
failed_activemerchant_billing_response(e.message)
|
||||
end
|
||||
|
||||
@@ -65,11 +84,17 @@ module Spree
|
||||
options.merge(login: Stripe.api_key)
|
||||
end
|
||||
|
||||
def options_for_purchase_or_auth(money, creditcard, gateway_options)
|
||||
def basic_options(gateway_options)
|
||||
options = {}
|
||||
options[:description] = "Spree Order ID: #{gateway_options[:order_id]}"
|
||||
options[:currency] = gateway_options[:currency]
|
||||
options[:stripe_account] = stripe_account_id
|
||||
options
|
||||
end
|
||||
|
||||
def options_for_authorize(money, creditcard, gateway_options)
|
||||
options = basic_options(gateway_options)
|
||||
options[:return_url] = full_checkout_path
|
||||
|
||||
customer_id, payment_method_id = Stripe::CreditCardCloner.new.clone(creditcard,
|
||||
stripe_account_id)
|
||||
@@ -77,6 +102,19 @@ module Spree
|
||||
[money, payment_method_id, options]
|
||||
end
|
||||
|
||||
def fetch_payment_intent(creditcard, gateway_options)
|
||||
payment = fetch_payment(creditcard, gateway_options)
|
||||
raise Stripe::StripeError, I18n.t(:no_pending_payments) unless payment&.response_code
|
||||
|
||||
Stripe::PaymentIntentValidator.new.call(payment.response_code, stripe_account_id)
|
||||
end
|
||||
|
||||
def fetch_payment(creditcard, gateway_options)
|
||||
order_number = gateway_options[:order_id].split('-').first
|
||||
|
||||
Spree::Order.find_by_number(order_number).payments.merge(creditcard.payments).last
|
||||
end
|
||||
|
||||
def failed_activemerchant_billing_response(error_message)
|
||||
ActiveMerchant::Billing::Response.new(false, error_message)
|
||||
end
|
||||
@@ -86,6 +124,16 @@ module Spree
|
||||
|
||||
errors.add(:stripe_account_owner, I18n.t(:error_required))
|
||||
end
|
||||
|
||||
def full_checkout_path
|
||||
URI.join(url_helpers.root_url, url_helpers.checkout_path).to_s
|
||||
end
|
||||
|
||||
def url_helpers
|
||||
# This is how we can get the helpers with a usable root_url outside the controllers
|
||||
Rails.application.routes.default_url_options = ActionMailer::Base.default_url_options
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -121,6 +121,12 @@ Spree::Order.class_eval do
|
||||
end
|
||||
end
|
||||
|
||||
# "Checkout" is the initial state and, for card payments, "pending" is the state after authorization
|
||||
# These are both valid states to process the payment
|
||||
def pending_payments
|
||||
(payments.select(&:pending?) + payments.select(&:processing?) + payments.select(&:checkout?)).uniq
|
||||
end
|
||||
|
||||
def remove_variant(variant)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Provides the redirect path if a redirect to the payment gateway is needed
|
||||
module Checkout
|
||||
class PaymentRedirect
|
||||
class PaypalRedirect
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
30
app/services/checkout/post_checkout_actions.rb
Normal file
30
app/services/checkout/post_checkout_actions.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Executes actions after checkout
|
||||
module Checkout
|
||||
class PostCheckoutActions
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def success(controller, params, current_user)
|
||||
save_order_addresses_as_user_default(params, current_user)
|
||||
ResetOrderService.new(controller, @order).call
|
||||
end
|
||||
|
||||
def failure
|
||||
@order.updater.shipping_address_from_distributor
|
||||
RestartCheckout.new(@order).call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_order_addresses_as_user_default(params, current_user)
|
||||
return unless params[:order]
|
||||
|
||||
user_default_address_setter = UserDefaultAddressSetter.new(@order, current_user)
|
||||
user_default_address_setter.set_default_bill_address if params[:order][:default_bill_address]
|
||||
user_default_address_setter.set_default_ship_address if params[:order][:default_ship_address]
|
||||
end
|
||||
end
|
||||
end
|
||||
46
app/services/checkout/stripe_redirect.rb
Normal file
46
app/services/checkout/stripe_redirect.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides the redirect path if a redirect to the payment gateway is needed
|
||||
module Checkout
|
||||
class StripeRedirect
|
||||
def initialize(params, order)
|
||||
@params = params
|
||||
@order = order
|
||||
end
|
||||
|
||||
# Returns the path to the authentication form if a redirect is needed
|
||||
def path
|
||||
return unless stripe_payment_method?
|
||||
|
||||
payment = @order.pending_payments.last
|
||||
return unless payment&.checkout?
|
||||
|
||||
payment.authorize!
|
||||
raise unless payment.pending?
|
||||
|
||||
field_with_url(payment) if url?(field_with_url(payment))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stripe_payment_method?
|
||||
return unless @params[:order][:payments_attributes]
|
||||
|
||||
payment_method_id = @params[:order][:payments_attributes].first[:payment_method_id]
|
||||
payment_method = Spree::PaymentMethod.find(payment_method_id)
|
||||
payment_method.is_a?(Spree::Gateway::StripeSCA)
|
||||
end
|
||||
|
||||
def url?(string)
|
||||
return false if string.blank?
|
||||
|
||||
string.starts_with?("http")
|
||||
end
|
||||
|
||||
# Stripe::AuthorizeResponsePatcher patches the Stripe authorization response
|
||||
# so that this field stores the redirect URL
|
||||
def field_with_url(payment)
|
||||
payment.cvv_response_message
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -275,7 +275,9 @@ en:
|
||||
none: None
|
||||
notes: Notes
|
||||
error: Error
|
||||
processing_payment: Processing payment...
|
||||
processing_payment: "Processing payment..."
|
||||
no_pending_payments: "No pending payments"
|
||||
invalid_payment_state: "Invalid payment state"
|
||||
filter_results: Filter Results
|
||||
quantity: Quantity
|
||||
pick_up: Pick up
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class ChangeCvvResponseMessageToTextInSpreePayments < ActiveRecord::Migration
|
||||
def up
|
||||
change_column :spree_payments, :cvv_response_message, :text
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :spree_payments, :cvv_response_message, :string
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20191202165700) do
|
||||
ActiveRecord::Schema.define(:version => 20200209163549) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -601,7 +601,7 @@ ActiveRecord::Schema.define(:version => 20191202165700) do
|
||||
t.string "avs_response"
|
||||
t.string "identifier"
|
||||
t.string "cvv_response_code"
|
||||
t.string "cvv_response_message"
|
||||
t.text "cvv_response_message"
|
||||
end
|
||||
|
||||
add_index "spree_payments", ["order_id"], :name => "index_spree_payments_on_order_id"
|
||||
|
||||
36
lib/stripe/authorize_response_patcher.rb
Normal file
36
lib/stripe/authorize_response_patcher.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This class patches the Stripe API response to the authorize action
|
||||
# It copies the authorization URL to a field that is recognized and persisted by Spree payments
|
||||
module Stripe
|
||||
class AuthorizeResponsePatcher
|
||||
def initialize(response)
|
||||
@response = response
|
||||
end
|
||||
|
||||
def call!
|
||||
if (url = url_for_authorization(@response)) && field_to_patch(@response).present?
|
||||
field_to_patch(@response)['message'] = url
|
||||
end
|
||||
|
||||
@response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def url_for_authorization(response)
|
||||
next_action = response.params["next_source_action"]
|
||||
return unless response.params["status"] == "requires_source_action" &&
|
||||
next_action.present? &&
|
||||
next_action["type"] == "authorize_with_url"
|
||||
|
||||
next_action["authorize_with_url"]["url"]
|
||||
end
|
||||
|
||||
# This field is used because the Spree code recognizes and stores it
|
||||
# This data is then used in Checkout::StripeRedirect
|
||||
def field_to_patch(response)
|
||||
response.cvv_result
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/stripe/payment_intent_validator.rb
Normal file
31
lib/stripe/payment_intent_validator.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This class validates if a given payment intent ID is valid in Stripe
|
||||
module Stripe
|
||||
class PaymentIntentValidator
|
||||
def call(payment_intent_id, stripe_account_id)
|
||||
payment_intent_response = Stripe::PaymentIntent.retrieve(payment_intent_id,
|
||||
stripe_account: stripe_account_id)
|
||||
|
||||
raise_if_last_payment_error_present(payment_intent_response)
|
||||
raise_if_not_in_capture_state(payment_intent_response)
|
||||
|
||||
payment_intent_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def raise_if_last_payment_error_present(payment_intent_response)
|
||||
return unless payment_intent_response.respond_to?(:last_payment_error) &&
|
||||
payment_intent_response.last_payment_error.present?
|
||||
|
||||
raise Stripe::StripeError, payment_intent_response.last_payment_error.message
|
||||
end
|
||||
|
||||
def raise_if_not_in_capture_state(payment_intent_response)
|
||||
return unless payment_intent_response.status != 'requires_capture'
|
||||
|
||||
raise Stripe::StripeError, I18n.t(:invalid_payment_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe CheckoutController, type: :controller do
|
||||
let(:distributor) { double(:distributor) }
|
||||
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:order) { create(:order) }
|
||||
let(:reset_order_service) { double(ResetOrderService) }
|
||||
@@ -36,7 +36,7 @@ describe CheckoutController, type: :controller do
|
||||
expect(flash[:info]).to eq("The hub you have selected is temporarily closed for orders. Please try again later.")
|
||||
end
|
||||
|
||||
describe "redirection to the cart" do
|
||||
describe "redirection to cart and stripe" do
|
||||
let(:order_cycle_distributed_variants) { double(:order_cycle_distributed_variants) }
|
||||
|
||||
before do
|
||||
@@ -44,7 +44,7 @@ describe CheckoutController, type: :controller do
|
||||
allow(order).to receive(:distributor).and_return(distributor)
|
||||
order.order_cycle = order_cycle
|
||||
|
||||
allow(OrderCycleDistributedVariants).to receive(:new).with(order_cycle, distributor).and_return(order_cycle_distributed_variants)
|
||||
allow(OrderCycleDistributedVariants).to receive(:new).and_return(order_cycle_distributed_variants)
|
||||
end
|
||||
|
||||
it "redirects when some items are out of stock" do
|
||||
@@ -62,12 +62,34 @@ describe CheckoutController, type: :controller do
|
||||
expect(response).to redirect_to cart_path
|
||||
end
|
||||
|
||||
it "does not redirect when items are available and in stock" do
|
||||
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return true
|
||||
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).with(order).and_return(true)
|
||||
describe "when items are available and in stock" do
|
||||
before do
|
||||
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return true
|
||||
end
|
||||
|
||||
get :edit
|
||||
expect(response).to be_success
|
||||
it "does not redirect" do
|
||||
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).with(order).and_return(true)
|
||||
get :edit
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
describe "when the order is in payment state and a stripe payment intent is provided" do
|
||||
before do
|
||||
order.update_attribute :state, "payment"
|
||||
order.ship_address = create(:address)
|
||||
order.save!
|
||||
order.payments << create(:payment, state: "pending", response_code: "pi_123")
|
||||
|
||||
# this is called a 2nd time after order completion from the reset_order_service
|
||||
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).twice.and_return(true)
|
||||
end
|
||||
|
||||
it "completes the order and redirects to the order confirmation page" do
|
||||
get :edit, { payment_intent: "pi_123" }
|
||||
expect(order.completed?).to be true
|
||||
expect(response).to redirect_to spree.order_path(order)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -238,23 +260,41 @@ describe CheckoutController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
describe "Paypal routing" do
|
||||
let(:payment_method) { create(:payment_method, type: "Spree::Gateway::PayPalExpress") }
|
||||
let(:restart_checkout) { instance_double(RestartCheckout, call: true) }
|
||||
|
||||
describe "Payment redirects" do
|
||||
before do
|
||||
allow(controller).to receive(:current_distributor) { distributor }
|
||||
allow(controller).to receive(:current_order_cycle) { order_cycle }
|
||||
allow(controller).to receive(:current_order) { order }
|
||||
|
||||
allow(RestartCheckout).to receive(:new) { restart_checkout }
|
||||
end
|
||||
|
||||
it "should check the payment method for Paypalness if we've selected one" do
|
||||
expect(Spree::PaymentMethod).to receive(:find).with(payment_method.id.to_s) { payment_method }
|
||||
allow(order).to receive(:update_attributes) { true }
|
||||
allow(order).to receive(:state) { "payment" }
|
||||
spree_post :update, order: { payments_attributes: [{ payment_method_id: payment_method.id }] }
|
||||
end
|
||||
|
||||
describe "paypal redirect" do
|
||||
let(:payment_method) { create(:payment_method, type: "Spree::Gateway::PayPalExpress") }
|
||||
let(:paypal_redirect) { instance_double(Checkout::PaypalRedirect) }
|
||||
|
||||
it "should call Paypal redirect and redirect if a path is provided" do
|
||||
expect(Checkout::PaypalRedirect).to receive(:new).and_return(paypal_redirect)
|
||||
expect(paypal_redirect).to receive(:path).and_return("test_path")
|
||||
|
||||
spree_post :update, order: { payments_attributes: [{ payment_method_id: payment_method.id }] }
|
||||
|
||||
expect(response.body).to eq({ path: "test_path" }.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe "stripe redirect" do
|
||||
let(:payment_method) { create(:payment_method, type: "Spree::Gateway::StripeSCA") }
|
||||
let(:stripe_redirect) { instance_double(Checkout::StripeRedirect) }
|
||||
|
||||
it "should call Stripe redirect and redirect if a path is provided" do
|
||||
expect(Checkout::StripeRedirect).to receive(:new).and_return(stripe_redirect)
|
||||
expect(stripe_redirect).to receive(:path).and_return("test_path")
|
||||
|
||||
spree_post :update, order: { payments_attributes: [{ payment_method_id: payment_method.id }] }
|
||||
|
||||
expect(response.body).to eq({ path: "test_path" }.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'open_food_network/subscription_payment_updater'
|
||||
|
||||
module OpenFoodNetwork
|
||||
@@ -9,12 +10,12 @@ module OpenFoodNetwork
|
||||
context "when only one payment exists on the order" do
|
||||
let!(:payment) { create(:payment, order: order) }
|
||||
|
||||
context "where the payment is in the 'checkout' state" do
|
||||
context "where the payment is pending" do
|
||||
it { expect(updater.send(:payment)).to eq payment }
|
||||
end
|
||||
|
||||
context "where the payment is in some other state" do
|
||||
before { payment.update_attribute(:state, 'pending') }
|
||||
context "where the payment is failed" do
|
||||
before { payment.update_attribute(:state, 'failed') }
|
||||
it { expect(updater.send(:payment)).to be nil }
|
||||
end
|
||||
end
|
||||
@@ -23,19 +24,19 @@ module OpenFoodNetwork
|
||||
let!(:payment1) { create(:payment, order: order) }
|
||||
let!(:payment2) { create(:payment, order: order) }
|
||||
|
||||
context "where more than one payment is in the 'checkout' state" do
|
||||
context "where more than one payment is pending" do
|
||||
it { expect([payment1, payment2]).to include updater.send(:payment) }
|
||||
end
|
||||
|
||||
context "where only one payment is in the 'checkout' state" do
|
||||
before { payment1.update_attribute(:state, 'pending') }
|
||||
context "where only one payment is pending" do
|
||||
before { payment1.update_attribute(:state, 'failed') }
|
||||
it { expect(updater.send(:payment)).to eq payment2 }
|
||||
end
|
||||
|
||||
context "where no payments are in the 'checkout' state" do
|
||||
context "where no payments are pending" do
|
||||
before do
|
||||
payment1.update_attribute(:state, 'pending')
|
||||
payment2.update_attribute(:state, 'pending')
|
||||
payment1.update_attribute(:state, 'failed')
|
||||
payment2.update_attribute(:state, 'failed')
|
||||
end
|
||||
|
||||
it { expect(updater.send(:payment)).to be nil }
|
||||
|
||||
33
spec/lib/stripe/authorize_response_patcher_spec.rb
Normal file
33
spec/lib/stripe/authorize_response_patcher_spec.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Stripe
|
||||
describe AuthorizeResponsePatcher do
|
||||
describe "#call!" do
|
||||
let(:patcher) { Stripe::AuthorizeResponsePatcher.new(response) }
|
||||
let(:params) { {} }
|
||||
let(:response) { ActiveMerchant::Billing::Response.new(true, "Transaction approved", params) }
|
||||
|
||||
context "when url not found in response" do
|
||||
it "does nothing" do
|
||||
new_response = patcher.call!
|
||||
expect(new_response).to eq response
|
||||
end
|
||||
end
|
||||
|
||||
context "when url is found in response" do
|
||||
let(:params) {
|
||||
{ "status" => "requires_source_action",
|
||||
"next_source_action" => { "type" => "authorize_with_url",
|
||||
"authorize_with_url" => { "url" => "test_url" } } }
|
||||
}
|
||||
|
||||
it "patches response.cvv_result.message with the url in the response" do
|
||||
new_response = patcher.call!
|
||||
expect(new_response.cvv_result['message']).to eq "test_url"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
60
spec/lib/stripe/payment_intent_validator_spec.rb
Normal file
60
spec/lib/stripe/payment_intent_validator_spec.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'stripe/payment_intent_validator'
|
||||
|
||||
module Stripe
|
||||
describe PaymentIntentValidator do
|
||||
describe "#call" do
|
||||
let(:validator) { Stripe::PaymentIntentValidator.new }
|
||||
let(:payment_intent_id) { "pi_123" }
|
||||
let(:stripe_account_id) { "acct_456" }
|
||||
let(:payment_intent_response_mock) { { status: 200, body: payment_intent_response_body } }
|
||||
|
||||
before do
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
|
||||
|
||||
stub_request(:get, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}")
|
||||
.with(headers: { 'Stripe-Account' => stripe_account_id })
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "when payment intent is valid" do
|
||||
let(:payment_intent_response_body) {
|
||||
JSON.generate(id: payment_intent_id, status: "requires_capture")
|
||||
}
|
||||
|
||||
it "returns payment intent id and does not raise" do
|
||||
expect {
|
||||
result = validator.call(payment_intent_id, stripe_account_id)
|
||||
expect(result).to eq payment_intent_id
|
||||
}.to_not raise_error Stripe::StripeError
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment intent status is not requires status" do
|
||||
let(:payment_intent_response_body) {
|
||||
JSON.generate(id: payment_intent_id, status: "failed")
|
||||
}
|
||||
|
||||
it "raises Stripe error with an invalid_payment_state message" do
|
||||
expect {
|
||||
validator.call(payment_intent_id, stripe_account_id)
|
||||
}.to raise_error Stripe::StripeError, "Invalid payment state"
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment intent contains an error" do
|
||||
let(:payment_intent_response_body) {
|
||||
JSON.generate(id: payment_intent_id, last_payment_error: { message: "No money" })
|
||||
}
|
||||
|
||||
it "raises Stripe error with payment intent last_payment_error as message" do
|
||||
expect {
|
||||
validator.call(payment_intent_id, stripe_account_id)
|
||||
}.to raise_error Stripe::StripeError, "No money"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,6 +22,8 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
let(:stripe_payment_method) { "pm_123" }
|
||||
let(:customer_id) { "cus_A123" }
|
||||
let(:hubs_stripe_payment_method) { "pm_456" }
|
||||
let(:payment_intent_id) { "pi_123" }
|
||||
let(:stripe_redirect_url) { "http://stripe.com/redirect" }
|
||||
let(:payments_attributes) do
|
||||
{
|
||||
payment_method_id: payment_method.id,
|
||||
@@ -62,6 +64,11 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
let(:payment_intent_response_mock) do
|
||||
{ status: 200, body: JSON.generate(object: "payment_intent", amount: 2000, charges: { data: [{ id: "ch_1234", amount: 2000 }]}) }
|
||||
end
|
||||
let(:payment_intent_authorize_response_mock) do
|
||||
{ status: 200, body: JSON.generate(id: payment_intent_id, object: "payment_intent", amount: 2000,
|
||||
status: "requires_capture", last_payment_error: nil,
|
||||
charges: { data: [{ id: "ch_1234", amount: 2000 }]}) }
|
||||
end
|
||||
|
||||
before do
|
||||
order_cycle_distributed_variants = double(:order_cycle_distributed_variants)
|
||||
@@ -72,6 +79,21 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
order.update_attributes(distributor_id: enterprise.id, order_cycle_id: order_cycle.id)
|
||||
order.reload.update_totals
|
||||
set_order order
|
||||
|
||||
# Authorizes the payment
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
|
||||
.to_return(payment_intent_authorize_response_mock)
|
||||
|
||||
# Retrieves payment intent info
|
||||
stub_request(:get, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}")
|
||||
.with(headers: { 'Stripe-Account' => 'abc123' })
|
||||
.to_return(payment_intent_authorize_response_mock)
|
||||
|
||||
# Captures the payment
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}/capture")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: { amount_to_capture: "1234" })
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "when the user submits a new card and doesn't request that the card is saved for later" do
|
||||
@@ -85,14 +107,9 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
.with(body: { payment_method: stripe_payment_method },
|
||||
headers: { 'Stripe-Account' => 'abc123' })
|
||||
.to_return(hubs_payment_method_response_mock)
|
||||
|
||||
# Charges the card
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: /#{hubs_stripe_payment_method}.*#{order.number}/)
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "and the paymeent intent request is successful" do
|
||||
context "and the payment intent request is successful" do
|
||||
it "should process the payment without storing card details" do
|
||||
put update_checkout_path, params
|
||||
|
||||
@@ -175,13 +192,6 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{stripe_payment_method}/attach")
|
||||
.with(body: { customer: customer_id })
|
||||
.to_return(payment_method_attach_response_mock)
|
||||
|
||||
# Charges the card
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
|
||||
.with(
|
||||
basic_auth: ["sk_test_12345", ""],
|
||||
body: /.*#{order.number}/
|
||||
).to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "and the customer, payment_method and payment_intent requests are successful" do
|
||||
@@ -267,11 +277,6 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
before do
|
||||
params[:order][:existing_card_id] = credit_card.id
|
||||
quick_login_as(order.user)
|
||||
|
||||
# Charges the card
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: %r{#{customer_id}.*#{hubs_stripe_payment_method}})
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "and the payment intent and payment method requests are accepted" do
|
||||
@@ -306,6 +311,21 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the stripe API sends a url for the authorization of the transaction" do
|
||||
let(:payment_intent_authorize_response_mock) do
|
||||
{ status: 200, body: JSON.generate(id: payment_intent_id, object: "payment_intent",
|
||||
next_source_action: { type: "authorize_with_url", authorize_with_url: { url: stripe_redirect_url }},
|
||||
status: "requires_source_action" )}
|
||||
end
|
||||
|
||||
it "redirects the user to the authorization stripe url" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 200
|
||||
expect(response.body).to include stripe_redirect_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Checkout::PaymentRedirect do
|
||||
describe '#order_params' do
|
||||
describe Checkout::PaypalRedirect do
|
||||
describe '#path' do
|
||||
let(:params) { { order: { order_id: "123" } } }
|
||||
|
||||
let(:redirect) { Checkout::PaymentRedirect.new(params) }
|
||||
let(:redirect) { Checkout::PaypalRedirect.new(params) }
|
||||
|
||||
it "returns nil if payment_attributes are not provided" do
|
||||
expect(redirect.path).to be nil
|
||||
66
spec/services/checkout/post_checkout_actions_spec.rb
Normal file
66
spec/services/checkout/post_checkout_actions_spec.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Checkout::PostCheckoutActions do
|
||||
let(:order) { create(:order_with_distributor) }
|
||||
let(:postCheckoutActions) { Checkout::PostCheckoutActions.new(order) }
|
||||
|
||||
describe "#success" do
|
||||
let(:controller) {}
|
||||
let(:params) { { order: {} } }
|
||||
let(:current_user) { order.distributor.owner }
|
||||
|
||||
let(:reset_order_service) { instance_double(ResetOrderService) }
|
||||
|
||||
before do
|
||||
expect(ResetOrderService).to receive(:new).
|
||||
with(controller, order).and_return(reset_order_service)
|
||||
expect(reset_order_service).to receive(:call)
|
||||
end
|
||||
|
||||
it "resets the order" do
|
||||
postCheckoutActions.success(controller, params, current_user)
|
||||
end
|
||||
|
||||
describe "setting the user default address" do
|
||||
let(:user_default_address_setter) { instance_double(UserDefaultAddressSetter) }
|
||||
|
||||
before do
|
||||
expect(UserDefaultAddressSetter).to receive(:new).
|
||||
with(order, current_user).and_return(user_default_address_setter)
|
||||
end
|
||||
|
||||
it "sets user default bill address is option selected in params" do
|
||||
params[:order][:default_bill_address] = true
|
||||
expect(user_default_address_setter).to receive(:set_default_bill_address)
|
||||
|
||||
postCheckoutActions.success(controller, params, current_user)
|
||||
end
|
||||
|
||||
it "sets user default ship address is option selected in params" do
|
||||
params[:order][:default_ship_address] = true
|
||||
expect(user_default_address_setter).to receive(:set_default_ship_address)
|
||||
|
||||
postCheckoutActions.success(controller, params, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#failure" do
|
||||
let(:restart_checkout_service) { instance_double(RestartCheckout) }
|
||||
|
||||
it "restarts the checkout process" do
|
||||
expect(RestartCheckout).to receive(:new).with(order).and_return(restart_checkout_service)
|
||||
expect(restart_checkout_service).to receive(:call)
|
||||
|
||||
postCheckoutActions.failure
|
||||
end
|
||||
|
||||
it "fixes the ship address for collection orders with the distributor's address" do
|
||||
expect(order.updater).to receive(:shipping_address_from_distributor)
|
||||
|
||||
postCheckoutActions.failure
|
||||
end
|
||||
end
|
||||
end
|
||||
55
spec/services/checkout/stripe_redirect_spec.rb
Normal file
55
spec/services/checkout/stripe_redirect_spec.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Checkout::StripeRedirect do
|
||||
describe '#path' do
|
||||
let(:order) { create(:order) }
|
||||
let(:params) { { order: { order_id: order.id } } }
|
||||
|
||||
let(:redirect) { Checkout::StripeRedirect.new(params, order) }
|
||||
|
||||
it "returns nil if payment_attributes are not provided" do
|
||||
expect(redirect.path).to be nil
|
||||
end
|
||||
|
||||
describe "when payment_attributes are provided" do
|
||||
it "raises an error if payment method does not exist" do
|
||||
params[:order][:payments_attributes] = [{ payment_method_id: "123" }]
|
||||
|
||||
expect { redirect.path }.to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
describe "when payment method provided exists" do
|
||||
before { params[:order][:payments_attributes] = [{ payment_method_id: payment_method.id }] }
|
||||
|
||||
describe "and the payment method is not a stripe payment method" do
|
||||
let(:payment_method) { create(:payment_method) }
|
||||
|
||||
it "returns nil" do
|
||||
expect(redirect.path).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "and the payment method is a stripe method" do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:payment_method) { create(:stripe_sca_payment_method) }
|
||||
|
||||
it "returns the redirect path" do
|
||||
stripe_payment = create(:payment, payment_method_id: payment_method.id)
|
||||
order.payments << stripe_payment
|
||||
allow(stripe_payment).to receive(:authorize!) do
|
||||
# Authorization moves the payment state from checkout/processing to pending
|
||||
stripe_payment.state = 'pending'
|
||||
true
|
||||
end
|
||||
test_redirect_url = "http://stripe_auth_url/"
|
||||
allow(stripe_payment).to receive(:cvv_response_message).and_return(test_redirect_url)
|
||||
|
||||
expect(redirect.path).to eq test_redirect_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user