Merge pull request #6151 from luisramos0/stripe_save_cards

Stripe Feature Tests: checkout with saved a card
This commit is contained in:
Matt-Yorkley
2020-10-24 23:29:29 +02:00
committed by GitHub
7 changed files with 183 additions and 115 deletions

View File

@@ -845,7 +845,6 @@ Metrics/ModuleLength:
- spec/models/spree/variant_spec.rb
- spec/services/permissions/order_spec.rb
- spec/services/variant_units/option_value_namer_spec.rb
- spec/support/request/stripe_helper.rb
Metrics/ParameterLists:
Max: 5

View File

@@ -5,6 +5,7 @@ require 'spree/core/gateway_error'
describe Spree::Admin::PaymentsController, type: :controller do
include StripeHelper
include StripeStubs
let!(:shop) { create(:enterprise) }
let!(:user) { shop.owner }

View File

@@ -8,6 +8,7 @@ feature '
' do
include AuthenticationHelper
include StripeHelper
include StripeStubs
let!(:order) { create(:completed_order_with_fees) }
let!(:stripe_payment_method) do

View File

@@ -7,6 +7,7 @@ feature "Check out with Stripe", js: true do
include ShopWorkflow
include CheckoutHelper
include StripeHelper
include StripeStubs
let(:distributor) { create(:distributor_enterprise) }
let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [variant]) }
@@ -90,12 +91,12 @@ feature "Check out with Stripe", js: true do
let!(:shipping_method) { create(:shipping_method) }
let(:error_message) { "Card was declined: insufficient funds." }
context "with guest checkout" do
before do
stub_payment_intent_get_request
stub_payment_methods_post_request
end
before do
stub_payment_intent_get_request
stub_payment_methods_post_request
end
context "with guest checkout" do
context "when the card is accepted" do
before do
stub_payment_intents_post_request order: order
@@ -126,7 +127,7 @@ feature "Check out with Stripe", js: true do
end
end
context "when the card needs extra SCA authorization", js: true do
context "when the card needs extra SCA authorization" do
before do
stripe_redirect_url = checkout_path(payment_intent: "pi_123")
stub_payment_intents_post_request_with_redirect order: order,
@@ -194,5 +195,49 @@ feature "Check out with Stripe", js: true do
end
end
end
context "with a logged in user" do
let(:user) { order.user }
before do
login_as user
end
context "saving a card and re-using it" do
before do
stub_payment_methods_post_request request: { payment_method: "pm_123", customer: "cus_A123" }, response: { pm_id: "pm_123" }
stub_payment_intents_post_request order: order
stub_successful_capture_request order: order
stub_customers_post_request email: user.email
stub_payment_method_attach_request
end
it "allows saving a card and re-using it" do
checkout_with_stripe guest_checkout: false, remember_card: true
expect(page).to have_content "Confirmed"
expect(order.reload.completed?).to eq true
expect(order.payments.first.state).to eq "completed"
# Verify card has been saved with correct stripe IDs
user_credit_card = order.reload.user.credit_cards.first
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
# Prepare a second order
new_order = create(:order, user: user, order_cycle: order_cycle, distributor: distributor, bill_address_id: nil, ship_address_id: nil)
set_order(new_order)
add_product_to_cart(new_order, product, quantity: 10)
# Checkout with saved card
visit checkout_path
choose free_shipping.name
choose stripe_sca_payment_method.name
expect(page).to have_content "Use a saved card"
expect(page).to have_select 'selected_card', selected: "Visa x-4242 Exp:10/2050"
place_order
end
end
end
end
end

View File

@@ -6,6 +6,8 @@ require 'stripe/credit_card_cloner'
module Stripe
describe CreditCardCloner do
describe "#clone" do
include StripeStubs
let(:cloner) { Stripe::CreditCardCloner.new }
let(:customer_id) { "cus_A123" }
@@ -13,7 +15,6 @@ module Stripe
let(:new_customer_id) { "cus_A456" }
let(:new_payment_method_id) { "pm_456" }
let(:stripe_account_id) { "acct_456" }
let(:customer_response_mock) { { status: 200, body: customer_response_body } }
let(:payment_method_response_mock) { { status: 200, body: payment_method_response_body } }
let(:credit_card) { create(:credit_card, user: create(:user)) }
@@ -21,18 +22,13 @@ module Stripe
let(:payment_method_response_body) {
JSON.generate(id: new_payment_method_id)
}
let(:customer_response_body) {
JSON.generate(id: new_customer_id)
}
before do
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
stub_request(:post, "https://api.stripe.com/v1/customers")
.with(body: { email: credit_card.user.email },
headers: { 'Stripe-Account' => stripe_account_id })
.to_return(customer_response_mock)
stub_customers_post_request email: credit_card.user.email,
response: { customer_id: new_customer_id },
stripe_account_header: true
stub_request(:post,
"https://api.stripe.com/v1/payment_methods/#{new_payment_method_id}/attach")
.with(body: { customer: new_customer_id },

View File

@@ -1,16 +1,16 @@
# frozen_string_literal: true
module StripeHelper
def checkout_with_stripe
def checkout_with_stripe(guest_checkout: true, remember_card: false)
visit checkout_path
checkout_as_guest
checkout_as_guest if guest_checkout
fill_out_form(
free_shipping.name,
stripe_sca_payment_method.name,
save_default_addresses: false
)
fill_out_card_details
check "Remember this card?" if remember_card
place_order
end
@@ -34,100 +34,4 @@ module StripeHelper
allow(Stripe).to receive(:publishable_key) { "pk_test_12345" }
Spree::Config.set(stripe_connect_enabled: true)
end
def stub_payment_intents_post_request(order:, response: {}, stripe_account_header: true)
stub = stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
stub = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header
stub.to_return(payment_intent_authorize_response_mock(response))
end
def stub_payment_intents_post_request_with_redirect(order:, redirect_url:)
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
.to_return(payment_intent_redirect_response_mock(redirect_url))
end
def stub_payment_intent_get_request(response: {}, stripe_account_header: true)
stub = stub_request(:get, "https://api.stripe.com/v1/payment_intents/pi_123")
stub = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header
stub.to_return(payment_intent_authorize_response_mock(response))
end
def stub_payment_methods_post_request(response: {})
stub_request(:post, "https://api.stripe.com/v1/payment_methods")
.with(body: { payment_method: "pm_123" },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(hub_payment_method_response_mock(response))
end
def stub_successful_capture_request(order:, response: {})
stub_capture_request(order, payment_successful_capture_mock(response))
end
def stub_failed_capture_request(order:, response: {})
stub_capture_request(order, payment_failed_capture_mock(response))
end
def stub_capture_request(order, response_mock)
stub_request(:post, "https://api.stripe.com/v1/payment_intents/pi_123/capture")
.with(body: { amount_to_capture: Spree::Money.new(order.total).cents },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(response_mock)
end
def stub_refund_request
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds")
.with(body: { amount: 2000, expand: ["charge"] },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(payment_successful_refund_mock)
end
private
def payment_intent_authorize_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: "pi_123",
object: "payment_intent",
amount: 2000,
amount_received: 2000,
status: options[:intent_status] || "requires_capture",
last_payment_error: nil,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_intent_redirect_response_mock(redirect_url)
{ status: 200, body: JSON.generate(id: "pi_123",
object: "payment_intent",
next_source_action: {
type: "authorize_with_url",
authorize_with_url: { url: redirect_url }
},
status: "requires_source_action") }
end
def payment_successful_capture_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(object: "payment_intent",
amount: 2000,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_failed_capture_mock(options)
{ status: options[:code] || 402,
body: JSON.generate(error: { message:
options[:message] || "payment-method-failure" }) }
end
def hub_payment_method_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: "pm_456", customer: "cus_A123") }
end
def payment_successful_refund_mock
{ status: 200,
body: JSON.generate(object: "refund",
amount: 2000,
charge: "ch_1234") }
end
end

View File

@@ -0,0 +1,122 @@
# frozen_string_literal: true
module StripeStubs
def stub_payment_intents_post_request(order:, response: {}, stripe_account_header: true)
stub = stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
stub = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header
stub.to_return(payment_intent_authorize_response_mock(response))
end
def stub_payment_intents_post_request_with_redirect(order:, redirect_url:)
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
.to_return(payment_intent_redirect_response_mock(redirect_url))
end
def stub_payment_intent_get_request(response: {}, stripe_account_header: true)
stub = stub_request(:get, "https://api.stripe.com/v1/payment_intents/pi_123")
stub = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header
stub.to_return(payment_intent_authorize_response_mock(response))
end
def stub_payment_methods_post_request(request: { payment_method: "pm_123" }, response: {})
stub_request(:post, "https://api.stripe.com/v1/payment_methods")
.with(body: request,
headers: { 'Stripe-Account' => 'abc123' })
.to_return(hub_payment_method_response_mock(response))
end
# Attaches the payment method to the customer in the hub's stripe account
def stub_payment_method_attach_request
stub_request(:post,
"https://api.stripe.com/v1/payment_methods/pm_123/attach")
.with(body: { customer: "cus_A123" })
.to_return(hub_payment_method_response_mock({ pm_id: "pm_123" }))
end
# Stubs the customers call to both the main stripe account and the connected account
def stub_customers_post_request(email:, response: {}, stripe_account_header: false)
stub = stub_request(:post, "https://api.stripe.com/v1/customers")
.with(body: { email: email })
stub = stub.with(headers: { 'Stripe-Account' => 'acct_456' }) if stripe_account_header
stub.to_return(customers_response_mock(response))
end
def stub_successful_capture_request(order:, response: {})
stub_capture_request(order, payment_successful_capture_mock(response))
end
def stub_failed_capture_request(order:, response: {})
stub_capture_request(order, payment_failed_capture_mock(response))
end
def stub_capture_request(order, response_mock)
stub_request(:post, "https://api.stripe.com/v1/payment_intents/pi_123/capture")
.with(body: { amount_to_capture: Spree::Money.new(order.total).cents },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(response_mock)
end
def stub_refund_request
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds")
.with(body: { amount: 2000, expand: ["charge"] },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(payment_successful_refund_mock)
end
private
def payment_intent_authorize_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: "pi_123",
object: "payment_intent",
amount: 2000,
amount_received: 2000,
status: options[:intent_status] || "requires_capture",
last_payment_error: nil,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_intent_redirect_response_mock(redirect_url)
{ status: 200, body: JSON.generate(id: "pi_123",
object: "payment_intent",
next_source_action: {
type: "authorize_with_url",
authorize_with_url: { url: redirect_url }
},
status: "requires_source_action") }
end
def payment_successful_capture_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(object: "payment_intent",
amount: 2000,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_failed_capture_mock(options)
{ status: options[:code] || 402,
body: JSON.generate(error: { message:
options[:message] || "payment-method-failure" }) }
end
def hub_payment_method_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: options[:pm_id] || "pm_456", customer: "cus_A123") }
end
def customers_response_mock(options)
customer_id = options[:customer_id] || "cus_A123"
{ status: 200,
body: JSON.generate(id: customer_id,
sources: { data: [id: customer_id] }) }
end
def payment_successful_refund_mock
{ status: 200,
body: JSON.generate(object: "refund",
amount: 2000,
charge: "ch_1234") }
end
end