Merge pull request #8779 from mkllnk/7872-remove-stripe-connect

Upgrade old Stripe Connect payment methods to Stripe SCA and remove Stripe Connect
This commit is contained in:
Filipe
2022-01-26 11:57:26 +00:00
committed by GitHub
19 changed files with 177 additions and 685 deletions

4
.env
View File

@@ -53,8 +53,8 @@ SMTP_PASSWORD="f00d"
# see="https://developers.google.com/maps/documentation/javascript/get-api-key
# GOOGLE_MAPS_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Stripe Connect details for instance account
# Find these under 'API keys' and 'Connect' in your Stripe account dashboard -> Account Settings
# Stripe details for instance account
# Find these under 'Developers' -> 'API keys' in your Stripe account dashboard.
# Under 'Connect', the Redirect URI should be set to https://YOUR_SERVER_URL/stripe/callbacks (e.g. https://openfoodnetwork.org.uk/stripe/callbacks)
# Under 'Webhooks', you should set up a Connect endpoint pointing to https://YOUR_SERVER_URL/stripe/webhooks e.g. (https://openfoodnetwork.org.uk/stripe/webhooks)
# STRIPE_INSTANCE_SECRET_KEY="sk_test_xxxxxx" # This can be a test key or a live key

View File

@@ -16,7 +16,7 @@ angular.module("admin.subscriptions").controller "DetailsController", ($scope, $
return if !newValue?
paymentMethod = ($scope.paymentMethods.filter (pm) -> pm.id == newValue)[0]
return unless paymentMethod?
$scope.cardRequired = (paymentMethod.type == "Spree::Gateway::StripeConnect" || paymentMethod.type == "Spree::Gateway::StripeSCA")
$scope.cardRequired = paymentMethod.type == "Spree::Gateway::StripeSCA"
$scope.loadCustomer() if $scope.cardRequired && !$scope.customer
$scope.loadCustomer = ->

View File

@@ -132,12 +132,6 @@ module Spree
providers.reject! { |provider| stripe_provider?(provider) }
end
# This method is deprecated and will be removed soon:
unless @payment_method&.type == "Spree::Gateway::StripeConnect" ||
OpenFoodNetwork::FeatureToggle.enabled?("StripeConnect")
providers.reject! { |provider| provider.name.ends_with?("StripeConnect") }
end
providers
end
@@ -160,12 +154,11 @@ module Spree
end
def stripe_payment_method?
["Spree::Gateway::StripeConnect",
"Spree::Gateway::StripeSCA"].include? @payment_method.try(:type)
@payment_method.try(:type) == "Spree::Gateway::StripeSCA"
end
def stripe_provider?(provider)
provider.name.ends_with?("StripeConnect", "StripeSCA")
provider.name.ends_with?("StripeSCA")
end
def base_params
@@ -177,7 +170,7 @@ module Spree
raw_params[ActiveModel::Naming.param_key(@payment_method)] || {}
end
# Merge payment method params with gateway params like :gateway_stripe_connect
# Merge payment method params with gateway params like :gateway_stripe_sca
# Also, remove password if present and blank
def update_params
@update_params ||= begin

View File

@@ -130,7 +130,7 @@ module Spree
preference :invoice_style2?, :boolean, default: false
preference :enable_receipt_printing?, :boolean, default: false
# Stripe Connect
# Stripe payments
preference :stripe_connect_enabled, :boolean, default: false
# Number localization

View File

@@ -1,109 +0,0 @@
# frozen_string_literal: true
require 'stripe/profile_storer'
module Spree
class Gateway
class StripeConnect < Gateway
preference :enterprise_id, :integer
validate :ensure_enterprise_selected
def method_type
'stripe'
end
def provider_class
ActiveMerchant::Billing::StripeGateway
end
def payment_profiles_supported?
true
end
def stripe_account_id
StripeAccount.find_by(enterprise_id: preferred_enterprise_id)&.stripe_user_id
end
# 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))
rescue Stripe::StripeError => e
# This will be an error caused by generating a stripe token
failed_activemerchant_billing_response(e.message)
end
def charge_offline(money, creditcard, gateway_options)
purchase(money, creditcard, gateway_options)
end
# NOTE: the name of this method is determined by Spree::Payment::Processing
def void(response_code, _creditcard, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.void(response_code, gateway_options)
end
# NOTE: the name of this method is determined by Spree::Payment::Processing
def credit(money, _creditcard, response_code, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(money, response_code, gateway_options)
end
def create_profile(payment)
return unless payment.source.gateway_customer_profile_id.nil?
profile_storer = Stripe::ProfileStorer.new(payment, provider)
profile_storer.create_customer_from_token
end
private
# In this gateway, what we call 'secret_key' is the 'login'
def options
options = super
options.merge(login: Stripe.api_key)
end
def options_for_purchase_or_auth(money, creditcard, gateway_options)
options = {}
options[:description] = "Spree Order ID: #{gateway_options[:order_id]}"
options[:currency] = gateway_options[:currency]
options[:stripe_account] = stripe_account_id
creditcard = token_from_card_profile_ids(creditcard)
[money, creditcard, options]
end
def token_from_card_profile_ids(creditcard)
token_or_card_id = creditcard.gateway_payment_profile_id
customer = creditcard.gateway_customer_profile_id
return nil if token_or_card_id.blank?
# Assume the gateway_payment_profile_id is a token generated by StripeJS
return token_or_card_id if customer.blank?
# Assume the gateway_payment_profile_id is a Stripe card_id
# So generate a new token, using the customer_id and card_id
tokenize_instance_customer_card(customer, token_or_card_id)
end
def tokenize_instance_customer_card(customer, card)
token = Stripe::Token.create({ card: card, customer: customer },
stripe_account: stripe_account_id)
token.id
end
def failed_activemerchant_billing_response(error_message)
ActiveMerchant::Billing::Response.new(false, error_message)
end
def ensure_enterprise_selected
return if preferred_enterprise_id&.positive?
errors.add(:stripe_account_owner, I18n.t(:error_required))
end
end
end
end

View File

@@ -6,8 +6,7 @@ module Api
delegate :serializable_hash, to: :method_serializer
def method_serializer
if object.type == 'Spree::Gateway::StripeConnect' ||
object.type == 'Spree::Gateway::StripeSCA'
if object.type == 'Spree::Gateway::StripeSCA'
Api::Admin::PaymentMethod::StripeSerializer.new(object)
else
Api::Admin::PaymentMethod::BaseSerializer.new(object)

View File

@@ -1,6 +1,4 @@
- case @payment_method
- when Spree::Gateway::StripeConnect
= render 'stripe_connect'
- when Spree::Gateway::StripeSCA
= render 'stripe_connect'
- else

View File

@@ -146,7 +146,6 @@ module Openfoodnetwork
# Register Spree payment methods
initializer "spree.gateway.payment_methods", :after => "spree.register.payment_methods" do |app|
Rails.application.reloader.to_prepare do
app.config.spree.payment_methods << Spree::Gateway::StripeConnect
app.config.spree.payment_methods << Spree::Gateway::StripeSCA
app.config.spree.payment_methods << Spree::Gateway::PayPalExpress
end

View File

@@ -0,0 +1,57 @@
class ConvertStripeConnectToStripeSca < ActiveRecord::Migration[6.1]
class SpreePreference < ActiveRecord::Base
scope :leftover_from_payment_type, ->(class_name) {
joins <<~SQL
JOIN spree_payment_methods
ON spree_preferences.key =
CONCAT('/#{class_name.underscore}/enterprise_id/', spree_payment_methods.id)
AND spree_payment_methods.type != '#{class_name}'
SQL
}
end
def up
delete_outdated_spree_preferences
upgrade_stripe_payment_methods
update_payment_method_preferences
end
private
# When changing the type of a payment method, we leave orphaned records in
# the spree_preferences table. The key of the preference contains the type
# of the payment method and therefore changing the type disconnects the
# preference.
#
# Here we delete orphaned preferences first so that we don't have any
# conflicts later when updating preferences alongside the connected
# payment methods.
def delete_outdated_spree_preferences
outdated_keys =
SpreePreference.leftover_from_payment_type("Spree::Gateway::StripeConnect").pluck(:key) +
SpreePreference.leftover_from_payment_type("Spree::Gateway::StripeSCA").pluck(:key)
SpreePreference.where(key: outdated_keys).delete_all
# Spree preferences are cached and we want to avoid reading old values.
# Danger: The cache may alter the given array in place. Make sure to
# not use the `outdated_keys` variable after this call.
Rails.cache.delete_multi(outdated_keys)
end
def upgrade_stripe_payment_methods
execute <<~SQL
UPDATE spree_payment_methods
SET type = 'Spree::Gateway::StripeSCA'
WHERE type = 'Spree::Gateway::StripeConnect'
SQL
end
def update_payment_method_preferences
execute <<~SQL
UPDATE spree_preferences
SET key = replace(key, 'stripe_connect', 'stripe_sca')
WHERE key LIKE '/spree/gateway/stripe_connect/%'
SQL
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_01_14_110920) do
ActiveRecord::Schema.define(version: 2022_01_18_053107) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

View File

@@ -28,8 +28,7 @@ module OrderManagement
end
def stripe_payment_method?
[Spree::Gateway::StripeConnect,
Spree::Gateway::StripeSCA].include? @payment.payment_method.class
@payment.payment_method.type == "Spree::Gateway::StripeSCA"
end
def card_set?

View File

@@ -93,8 +93,7 @@ module OrderManagement
end
def stripe_payment_method?(payment_method)
payment_method.type == "Spree::Gateway::StripeConnect" ||
payment_method.type == "Spree::Gateway::StripeSCA"
payment_method.type == "Spree::Gateway::StripeSCA"
end
def subscription_line_items_present?

View File

@@ -16,126 +16,6 @@ describe Spree::Admin::PaymentsController, type: :controller do
order.reload.update_totals
end
context "Stripe Connect" do
context "requesting a refund on a payment" do
let(:params) { { id: payment.id, order_id: order.number, e: :void } }
# Required for the respond override in the controller decorator to work
before { @request.env['HTTP_REFERER'] = spree.admin_order_payments_url(payment) }
context "that was processed by stripe" do
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:payment) do
create(:payment, :completed, order: order, payment_method: payment_method,
response_code: 'ch_1a2b3c', amount: order.total)
end
before do
Stripe.api_key = "sk_test_12345"
end
context "where the request succeeds" do
before do
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1a2b3c/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
end
it "voids the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'void'
order.reload
expect(order.payment_total).to eq 0
expect(order.outstanding_balance.to_f).to_not eq 0
end
end
context "where the request fails" do
before do
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1a2b3c/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200, body: JSON.generate(error: { message: "Bup-bow!" }) )
end
it "does not void the payment" do
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
spree_put :fire, params
expect(payment.reload.state).to eq 'completed'
order.reload
expect(order.payment_total).to_not eq 0
expect(order.outstanding_balance.to_f).to eq 0
expect(flash[:error]).to eq "Bup-bow!"
end
end
end
end
context "requesting a partial credit on a payment" do
let(:params) { { id: payment.id, order_id: order.number, e: :credit } }
# Required for the respond override in the controller decorator to work
before { @request.env['HTTP_REFERER'] = spree.admin_order_payments_url(payment) }
context "that was processed by stripe" do
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:payment) do
create(:payment, :completed, order: order, payment_method: payment_method,
response_code: 'ch_1a2b3c', amount: order.total + 5)
end
before do
Stripe.api_key = "sk_test_12345"
end
context "where the request succeeds" do
before do
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1a2b3c/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200,
body: JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') )
end
it "partially refunds the payment" do
order.reload
expect(order.payment_total).to eq order.total + 5
expect(order.outstanding_balance.to_f).to eq(-5)
spree_put :fire, params
expect(payment.reload.state).to eq 'completed'
order.reload
expect(order.payment_total).to eq order.total
expect(order.outstanding_balance.to_f).to eq 0
end
end
context "where the request fails" do
before do
stub_request(:post, "https://api.stripe.com/v1/charges/ch_1a2b3c/refunds").
with(basic_auth: ["sk_test_12345", ""]).
to_return(status: 200, body: JSON.generate(error: { message: "Bup-bow!" }) )
end
it "does not void the payment" do
order.reload
expect(order.payment_total).to eq order.total + 5
expect(order.outstanding_balance.to_f).to eq(-5)
spree_put :fire, params
expect(payment.reload.state).to eq 'completed'
order.reload
expect(order.payment_total).to eq order.total + 5
expect(order.outstanding_balance.to_f).to eq(-5)
expect(flash[:error]).to eq "Bup-bow!"
end
end
end
end
end
context "StripeSCA" do
context "voiding a payment" do
let(:params) { { id: payment.id, order_id: order.number, e: :void } }

View File

@@ -34,14 +34,13 @@ module Spree
providers = assigns(:providers).map(&:to_s)
expect(providers).to include "Spree::Gateway::StripeSCA"
expect(providers).to_not include "Spree::Gateway::StripeConnect"
end
end
describe "#edit" do
let(:deprecated_stripe) {
let(:stripe) {
create(
:stripe_connect_payment_method,
:stripe_sca_payment_method,
distributor_ids: [enterprise_id],
preferred_enterprise_id: enterprise_id
)
@@ -50,14 +49,13 @@ module Spree
before { allow(controller).to receive(:spree_current_user) { user } }
it "shows the current gateway type even if deprecated" do
allow(Spree::Config).to receive(:stripe_connect_enabled).and_return(true)
it "shows the current gateway type even if not enabled" do
allow(Spree::Config).to receive(:stripe_connect_enabled).and_return(false)
spree_get :edit, id: deprecated_stripe.id
spree_get :edit, id: stripe.id
providers = assigns(:providers).map(&:to_s)
expect(providers).to include "Spree::Gateway::StripeSCA"
expect(providers).to include "Spree::Gateway::StripeConnect"
end
end

View File

@@ -23,13 +23,6 @@ FactoryBot.define do
environment { 'test' }
end
factory :stripe_connect_payment_method, class: Spree::Gateway::StripeConnect do
name { 'StripeConnect' }
environment { 'test' }
distributors { [FactoryBot.create(:enterprise)] }
preferred_enterprise_id { distributors.first.id }
end
factory :stripe_sca_payment_method, class: Spree::Gateway::StripeSCA do
name { 'StripeSCA' }
environment { 'test' }

View File

@@ -0,0 +1,104 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca'
module Spree
class Gateway
class StripeConnect < Gateway::StripeSCA
# This class got deleted from the code base but this minimum definition
# is enough for this test.
end
end
end
describe ConvertStripeConnectToStripeSca do
let(:owner) { create(:distributor_enterprise) }
let(:new_owner) { create(:distributor_enterprise) }
let(:old_stripe_connect) {
Spree::Gateway::StripeConnect.create!(
name: "Stripe",
environment: "test",
preferred_enterprise_id: owner.id,
distributor_ids: [owner.id]
)
}
let(:result) { Spree::PaymentMethod.find(old_stripe_connect.id) }
before do
# Activate the cache because it's deactivated in test environment.
allow(Spree::Preferences::Store.instance).to receive(:should_persist?).and_return(true)
# Create the payment method after cache activation to store the owner.
old_stripe_connect
end
it "converts payment methods" do
subject.up
expect(result.class).to eq Spree::Gateway::StripeSCA
end
it "keeps attributes" do
subject.up
expect(result.name).to eq "Stripe"
expect(result.environment).to eq "test"
expect(result.distributor_ids).to eq [owner.id]
end
it "keeps Spree preferences" do
subject.up
expect(result.preferred_enterprise_id).to eq owner.id
end
it "doesn't move outdated StripeConnect preferences to StripeSCA methods" do
# When you change the type of a payment method in the admin screen
# it leaves old entries in the spree_preferences table.
# Here is a simulation of such a change:
old_stripe_connect.update_columns(type: "Spree::Gateway::StripeSCA")
changed_method = Spree::PaymentMethod.find(old_stripe_connect.id)
changed_method.preferred_enterprise_id = new_owner.id
subject.up
expect(result.preferred_enterprise_id).to eq new_owner.id
end
it "keeps Spree preferences despite conflicting preference keys" do
# We change the payment method to StripeSCA and then back to StripeConnect
# to generate a conflicting preference. We want to keep the preference
# of the current payment method, not the intermediately changed one.
old_stripe_connect.update_columns(type: "Spree::Gateway::StripeSCA")
changed_method = Spree::PaymentMethod.find(old_stripe_connect.id)
changed_method.preferred_enterprise_id = owner.id
old_stripe_connect.update_columns(type: "Spree::Gateway::StripeConnect")
old_stripe_connect.preferred_enterprise_id = new_owner.id
subject.up
expect(result.preferred_enterprise_id).to eq new_owner.id
end
it "doesn't mess with new Stripe payment methods" do
stripe = Spree::Gateway::StripeSCA.create!(
name: "Modern Stripe",
environment: "test",
preferred_enterprise_id: owner.id,
distributor_ids: [owner.id]
)
expect { subject.up }.to_not change { stripe.reload.attributes }
end
it "doesn't mess with other payment methods" do
cash = Spree::PaymentMethod::Check.create!(
name: "Cash on delivery",
environment: "test",
distributor_ids: [owner.id]
)
expect { subject.up }.to_not change { cash.reload.attributes }
end
end

View File

@@ -1,107 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
describe Spree::Gateway::StripeConnect, type: :model do
let(:provider) do
instance_double(ActiveMerchant::Billing::StripeGateway).tap do |p|
allow(p).to receive(:purchase)
allow(p).to receive(:authorize)
allow(p).to receive(:capture)
allow(p).to receive(:refund)
end
end
let(:stripe_account_id) { "acct_123" }
before do
Stripe.api_key = "sk_test_123456"
allow(subject).to receive(:stripe_account_id) { stripe_account_id }
allow(subject).to receive(:options_for_purchase_or_auth).and_return(['money', 'cc', 'opts'])
allow(subject).to receive(:provider).and_return provider
end
describe "#token_from_card_profile_ids" do
let(:creditcard) { double(:creditcard) }
context "when the credit card provided has a gateway_payment_profile_id" do
before do
allow(creditcard).to receive(:gateway_payment_profile_id) { "token_or_card_id123" }
allow(subject).to receive(:tokenize_instance_customer_card) { "tokenized" }
end
context "when the credit card provided has a gateway_customer_profile_id" do
before { allow(creditcard).to receive(:gateway_customer_profile_id) { "customer_id123" } }
it "requests a new token via tokenize_instance_customer_card" do
result = subject.send(:token_from_card_profile_ids, creditcard)
expect(result).to eq "tokenized"
end
end
context "when the credit card provided does not have a gateway_customer_profile_id" do
before { allow(creditcard).to receive(:gateway_customer_profile_id) { nil } }
it "returns the gateway_payment_profile_id (assumed to be a token already)" do
result = subject.send(:token_from_card_profile_ids, creditcard)
expect(result).to eq "token_or_card_id123"
end
end
end
context "when the credit card provided does not have a gateway_payment_profile_id" do
before { allow(creditcard).to receive(:gateway_payment_profile_id) { nil } }
before { allow(creditcard).to receive(:gateway_customer_profile_id) { "customer_id123" } }
it "returns nil....?" do
result = subject.send(:token_from_card_profile_ids, creditcard)
expect(result).to be nil
end
end
end
describe "#tokenize_instance_customer_card" do
let(:customer_id) { "customer123" }
let(:card_id) { "card123" }
let(:token_mock) { { id: "test_token123" } }
before do
stub_request(:post, "https://api.stripe.com/v1/tokens")
.with(body: { "card" => "card123", "customer" => "customer123" })
.to_return(body: JSON.generate(token_mock))
end
it "requests a new token for the customer and card from Stripe, and returns the id of the response" do
expect(subject.send(:tokenize_instance_customer_card, customer_id,
card_id)).to eq token_mock[:id]
end
end
describe "#credit" do
let(:gateway_options) { { some: 'option' } }
let(:money) { double(:money) }
let(:response_code) { double(:response_code) }
before do
subject.credit(money, double(:creditcard), response_code, gateway_options)
end
it "delegates to ActiveMerchant::Billing::StripeGateway#refund" do
expect(provider).to have_received(:refund)
end
it "adds the stripe_account to the gateway options hash" do
expect(provider).to have_received(:refund).with(money, response_code,
hash_including(stripe_account: stripe_account_id))
end
end
describe "#charging offline" do
let(:gateway_options) { { some: 'option' } }
let(:money) { double(:money) }
let(:card) { double(:creditcard) }
it "uses #purchase to charge offline" do
subject.charge_offline(money, card, gateway_options)
expect(provider).to have_received(:purchase).with('money', 'cc', 'opts')
end
end
end

View File

@@ -60,7 +60,6 @@ module Spree
it "generates a clean name for known Payment Method types" do
expect(Spree::PaymentMethod::Check.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.check"))
expect(Spree::Gateway::PayPalExpress.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.paypalexpress"))
expect(Spree::Gateway::StripeConnect.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.stripeconnect"))
expect(Spree::Gateway::StripeSCA.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.stripesca"))
expect(Spree::Gateway::BogusSimple.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.bogussimple"))
expect(Spree::Gateway::Bogus.clean_name).to eq(I18n.t("spree.admin.payment_methods.providers.bogus"))

View File

@@ -1,310 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
describe "checking out an order with a Stripe Connect payment method", type: :request do
include ShopWorkflow
include AuthenticationHelper
include OpenFoodNetwork::ApiHelper
let!(:order_cycle) { create(:simple_order_cycle) }
let!(:enterprise) { create(:distributor_enterprise) }
let!(:shipping_method) do
create(
:shipping_method,
calculator: Calculator::FlatRate.new(preferred_amount: 0),
distributors: [enterprise]
)
end
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [enterprise]) }
let!(:stripe_account) { create(:stripe_account, enterprise: enterprise) }
let!(:line_item) { create(:line_item, price: 12.34) }
let!(:order) { line_item.order }
let(:address) { create(:address) }
let(:token) { "token123" }
let(:new_token) { "newtoken123" }
let(:card_id) { "card_XyZ456" }
let(:customer_id) { "cus_A123" }
let(:payments_attributes) do
{
payment_method_id: payment_method.id,
source_attributes: {
gateway_payment_profile_id: token,
cc_type: "visa",
last_digits: "4242",
month: 10,
year: 2025,
first_name: 'Jill',
last_name: 'Jeffreys'
}
}
end
let(:allowed_address_attributes) do
[
"firstname",
"lastname",
"address1",
"address2",
"phone",
"city",
"zipcode",
"state_id",
"country_id"
]
end
let(:params) do
{
format: :json, order: {
shipping_method_id: shipping_method.id,
payments_attributes: [payments_attributes],
bill_address_attributes: address.attributes.slice(*allowed_address_attributes),
ship_address_attributes: address.attributes.slice(*allowed_address_attributes)
}
}
end
before do
order_cycle_distributed_variants = double(:order_cycle_distributed_variants)
allow(OrderCycleDistributedVariants).to receive(:new) { order_cycle_distributed_variants }
allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?) { true }
Stripe.api_key = "sk_test_12345"
order.update(distributor_id: enterprise.id, order_cycle_id: order_cycle.id)
order.reload.update_totals
set_order order
end
context "when a new card is submitted" do
let(:store_response_mock) do
{
status: 200,
body: JSON.generate(
id: customer_id,
default_card: card_id,
sources: { data: [{ id: "1" }] }
)
}
end
let(:token_response_mock) do
{ status: 200, body: JSON.generate(id: new_token) }
end
let(:charge_response_mock) do
{ status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) }
end
context "and the user doesn't request that the card is saved for later" do
before do
# Charges the card
stub_request(:post, "https://api.stripe.com/v1/charges")
.with(basic_auth: ["sk_test_12345", ""], body: /#{token}.*#{order.number}/)
.to_return(charge_response_mock)
end
context "and the charge request is successful" do
it "should process the payment without storing card details" do
put update_checkout_path, params: params
expect(json_response["path"]).to eq order_path(order, order_token: order.token)
expect(order.payments.completed.count).to be 1
card = order.payments.completed.first.source
expect(card.gateway_customer_profile_id).to eq nil
expect(card.gateway_payment_profile_id).to eq token
expect(card.cc_type).to eq "visa"
expect(card.last_digits).to eq "4242"
expect(card.first_name).to eq "Jill"
expect(card.last_name).to eq "Jeffreys"
end
end
context "when the charge request returns an error message" do
let(:charge_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
end
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"]).to eq "charge-failure"
expect(order.payments.completed.count).to be 0
end
end
end
context "and the customer requests that the card is saved for later" do
before do
source_attributes = params[:order][:payments_attributes][0][:source_attributes]
source_attributes[:save_requested_by_customer] = '1'
# Saves the card against the user
stub_request(:post, "https://api.stripe.com/v1/customers")
.with(basic_auth: ["sk_test_12345", ""], body: { card: token, email: order.email })
.to_return(store_response_mock)
# Requests a token from the newly saved card
stub_request(:post, "https://api.stripe.com/v1/tokens")
.with(body: { card: card_id, customer: customer_id })
.to_return(token_response_mock)
# Charges the card
stub_request(:post, "https://api.stripe.com/v1/charges")
.with(
basic_auth: ["sk_test_12345", ""],
body: /#{token}.*#{order.number}/
).to_return(charge_response_mock)
end
context "and the store, token and charge requests are successful" do
it "should process the payment, and stores the card/customer details" do
put update_checkout_path, params: params
expect(json_response["path"]).to eq order_path(order, order_token: order.token)
expect(order.payments.completed.count).to be 1
card = order.payments.completed.first.source
expect(card.gateway_customer_profile_id).to eq customer_id
expect(card.gateway_payment_profile_id).to eq card_id
expect(card.cc_type).to eq "visa"
expect(card.last_digits).to eq "4242"
expect(card.first_name).to eq "Jill"
expect(card.last_name).to eq "Jeffreys"
end
end
context "when the store request returns an error message" do
let(:store_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "store-failure" }) }
end
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"])
.to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure'))
expect(order.payments.completed.count).to be 0
end
end
context "when the charge request returns an error message" do
let(:charge_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
end
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"]).to eq "charge-failure"
expect(order.payments.completed.count).to be 0
end
end
context "when the token request returns an error message" do
let(:token_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "token-failure" }) }
end
# Note, no requests have been stubbed
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"]).to eq "token-failure"
expect(order.payments.completed.count).to be 0
end
end
end
end
context "when an existing card is submitted" do
let(:credit_card) do
create(
:credit_card,
user_id: order.user_id,
gateway_payment_profile_id: card_id,
gateway_customer_profile_id: customer_id,
last_digits: "4321",
cc_type: "master",
first_name: "Sammy",
last_name: "Signpost",
month: 11, year: 2026
)
end
let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } }
let(:charge_response_mock) do
{ status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) }
end
before do
params[:order][:existing_card_id] = credit_card.id
login_as(order.user)
# Requests a token
stub_request(:post, "https://api.stripe.com/v1/tokens")
.with(body: { "card" => card_id, "customer" => customer_id })
.to_return(token_response_mock)
# Charges the card
stub_request(:post, "https://api.stripe.com/v1/charges")
.with(basic_auth: ["sk_test_12345", ""], body: /#{token}.*#{order.number}/)
.to_return(charge_response_mock)
end
context "and the charge and token requests are accepted" do
it "should process the payment, and keep the profile ids and other card details" do
put update_checkout_path, params: params
expect(json_response["path"]).to eq order_path(order, order_token: order.token)
expect(order.payments.completed.count).to be 1
card = order.payments.completed.first.source
expect(card.gateway_customer_profile_id).to eq customer_id
expect(card.gateway_payment_profile_id).to eq card_id
expect(card.cc_type).to eq "master"
expect(card.last_digits).to eq "4321"
expect(card.first_name).to eq "Sammy"
expect(card.last_name).to eq "Signpost"
end
end
context "when the charge request returns an error message" do
let(:charge_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
end
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"]).to eq "charge-failure"
expect(order.payments.completed.count).to be 0
end
end
context "when the token request returns an error message" do
let(:token_response_mock) do
{ status: 402, body: JSON.generate(error: { message: "token-error" }) }
end
it "should not process the payment" do
put update_checkout_path, params: params
expect(response.status).to be 400
expect(json_response["flash"]["error"]).to eq "token-error"
expect(order.payments.completed.count).to be 0
end
end
end
end