mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-05 22:26:07 +00:00
Merge pull request #6469 from andrewpbrett/sca-backend
Allow SCA cards to be setup and charged offline for subscriptions
This commit is contained in:
@@ -11,8 +11,7 @@ Darkswarm.factory 'CreditCards', ($http, $filter, savedCreditCards, Messages, Cu
|
||||
othercard.is_default = false
|
||||
$http.put("/credit_cards/#{card.id}", is_default: true).then (data) ->
|
||||
Messages.success(t('js.default_card_updated'))
|
||||
for customer in Customers.index()
|
||||
customer.allow_charges = false
|
||||
Customers.clearAllAllowCharges()
|
||||
, (response) ->
|
||||
Messages.flash(response.data.flash)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("Darkswarm").factory 'Customer', ($resource, Messages) ->
|
||||
angular.module("Darkswarm").factory 'Customer', ($resource, $injector, Messages) ->
|
||||
Customer = $resource('/api/customers/:id/:action.json', {}, {
|
||||
'index':
|
||||
method: 'GET'
|
||||
@@ -12,8 +12,21 @@ angular.module("Darkswarm").factory 'Customer', ($resource, Messages) ->
|
||||
})
|
||||
|
||||
Customer.prototype.update = ->
|
||||
if @allow_charges
|
||||
Messages.loading(t('js.authorising'))
|
||||
@$update().then (response) =>
|
||||
Messages.success(t('js.changes_saved'))
|
||||
if response.gateway_recurring_payment_client_secret && $injector.has('stripePublishableKey')
|
||||
Messages.clear()
|
||||
stripe = Stripe($injector.get('stripePublishableKey'), { stripeAccount: response.gateway_shop_id })
|
||||
stripe.confirmCardSetup(response.gateway_recurring_payment_client_secret).then (result) =>
|
||||
if result.error
|
||||
@allow_charges = false
|
||||
@$update(allow_charges: false)
|
||||
Messages.error(result.error.message)
|
||||
else
|
||||
Messages.success(t('js.changes_saved'))
|
||||
else
|
||||
Messages.success(t('js.changes_saved'))
|
||||
, (response) =>
|
||||
Messages.error(response.data.error)
|
||||
|
||||
|
||||
@@ -12,3 +12,7 @@ angular.module("Darkswarm").factory 'Customers', (Customer) ->
|
||||
for customer in customers
|
||||
@all.push customer
|
||||
@byID[customer.id] = customer
|
||||
|
||||
clearAllAllowCharges: () ->
|
||||
for customer in @index()
|
||||
customer.allow_charges = false
|
||||
|
||||
@@ -11,13 +11,25 @@ module Api
|
||||
@customer = Customer.find(params[:id])
|
||||
authorize! :update, @customer
|
||||
|
||||
client_secret = RecurringPayments.setup_for(@customer) if params[:customer][:allow_charges]
|
||||
|
||||
if @customer.update(customer_params)
|
||||
add_recurring_payment_info(client_secret)
|
||||
render json: @customer, serializer: CustomerSerializer, status: :ok
|
||||
else
|
||||
invalid_resource!(@customer)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_recurring_payment_info(client_secret)
|
||||
return unless client_secret
|
||||
|
||||
@customer.gateway_recurring_payment_client_secret = client_secret
|
||||
@customer.gateway_shop_id = @customer.enterprise.stripe_account&.stripe_user_id
|
||||
end
|
||||
|
||||
def customer_params
|
||||
params.require(:customer).permit(:code, :email, :enterprise_id, :allow_charges)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'stripe/credit_card_clone_destroyer'
|
||||
|
||||
module Spree
|
||||
class CreditCardsController < BaseController
|
||||
def new_from_token
|
||||
@@ -45,6 +47,7 @@ module Spree
|
||||
|
||||
# Using try because we may not have a card here
|
||||
if @credit_card.try(:destroy)
|
||||
remove_shop_authorizations if @credit_card.is_default
|
||||
flash[:success] = I18n.t(:card_has_been_removed, number: "x-#{@credit_card.last_digits}")
|
||||
else
|
||||
flash[:error] = I18n.t(:card_could_not_be_removed)
|
||||
@@ -63,16 +66,10 @@ module Spree
|
||||
|
||||
# It destroys the whole customer object
|
||||
def destroy_at_stripe
|
||||
Stripe::CreditCardCloneDestroyer.new.destroy_clones(@credit_card)
|
||||
|
||||
stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id, {})
|
||||
|
||||
stripe_customer&.delete
|
||||
end
|
||||
|
||||
def stripe_account_id
|
||||
StripeAccount.
|
||||
find_by(enterprise_id: @credit_card.payment_method.preferred_enterprise_id).
|
||||
andand.
|
||||
stripe_user_id
|
||||
stripe_customer&.delete unless stripe_customer.deleted?
|
||||
end
|
||||
|
||||
def create_customer(token)
|
||||
|
||||
@@ -62,7 +62,7 @@ class SubscriptionConfirmJob
|
||||
return unless order.payment_required?
|
||||
|
||||
prepare_for_payment!(order)
|
||||
order.process_payments!
|
||||
order.process_payments_offline!
|
||||
raise if order.errors.any?
|
||||
end
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ class Customer < ActiveRecord::Base
|
||||
|
||||
before_create :associate_user
|
||||
|
||||
attr_accessor :gateway_recurring_payment_client_secret
|
||||
attr_accessor :gateway_shop_id
|
||||
|
||||
private
|
||||
|
||||
def downcase_email
|
||||
|
||||
@@ -45,11 +45,21 @@ module Spree
|
||||
failed_activemerchant_billing_response(e.message)
|
||||
end
|
||||
|
||||
# NOTE: the name of this method is determined by Spree::Payment::Processing
|
||||
def charge_offline(money, creditcard, gateway_options)
|
||||
customer, payment_method =
|
||||
Stripe::CreditCardCloner.new(creditcard, stripe_account_id).find_or_clone
|
||||
|
||||
options = basic_options(gateway_options).merge(customer: customer, off_session: true)
|
||||
provider.purchase(money, payment_method, 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))
|
||||
authorize_response =
|
||||
provider.authorize(*options_for_authorize(money, creditcard, gateway_options))
|
||||
Stripe::AuthorizeResponsePatcher.new(authorize_response).call!
|
||||
rescue Stripe::StripeError => e
|
||||
failed_activemerchant_billing_response(e.message)
|
||||
@@ -97,8 +107,8 @@ module Spree
|
||||
options = basic_options(gateway_options)
|
||||
options[:return_url] = full_checkout_path
|
||||
|
||||
customer_id, payment_method_id = Stripe::CreditCardCloner.new.clone(creditcard,
|
||||
stripe_account_id)
|
||||
customer_id, payment_method_id =
|
||||
Stripe::CreditCardCloner.new(creditcard, stripe_account_id).find_or_clone
|
||||
options[:customer] = customer_id
|
||||
[money, payment_method_id, options]
|
||||
end
|
||||
|
||||
@@ -500,22 +500,19 @@ module Spree
|
||||
# :allow_checkout_on_gateway_error is set to false
|
||||
#
|
||||
def process_payments!
|
||||
raise Core::GatewayError, Spree.t(:no_pending_payments) if pending_payments.empty?
|
||||
|
||||
pending_payments.each do |payment|
|
||||
break if payment_total >= total
|
||||
|
||||
payment.process!
|
||||
|
||||
if payment.completed?
|
||||
self.payment_total += payment.amount
|
||||
end
|
||||
end
|
||||
process_each_payment(&:process!)
|
||||
rescue Core::GatewayError => e
|
||||
result = !!Spree::Config[:allow_checkout_on_gateway_error]
|
||||
errors.add(:base, e.message) && (return result)
|
||||
end
|
||||
|
||||
def process_payments_offline!
|
||||
process_each_payment(&:process_offline!)
|
||||
rescue Core::GatewayError => e
|
||||
errors.add(:base, e.message)
|
||||
false
|
||||
end
|
||||
|
||||
def billing_firstname
|
||||
bill_address.try(:firstname)
|
||||
end
|
||||
@@ -778,6 +775,20 @@ module Spree
|
||||
|
||||
private
|
||||
|
||||
def process_each_payment
|
||||
raise Core::GatewayError, Spree.t(:no_pending_payments) if pending_payments.empty?
|
||||
|
||||
pending_payments.each do |payment|
|
||||
break if payment_total >= total
|
||||
|
||||
yield payment
|
||||
|
||||
if payment.completed?
|
||||
self.payment_total += payment.amount
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_by_email
|
||||
self.email = user.email if user
|
||||
end
|
||||
|
||||
@@ -4,16 +4,7 @@ module Spree
|
||||
class Payment < ActiveRecord::Base
|
||||
module Processing
|
||||
def process!
|
||||
return unless payment_method&.source_required?
|
||||
|
||||
raise Core::GatewayError, Spree.t(:payment_processing_failed) unless source
|
||||
|
||||
return if processing?
|
||||
|
||||
unless payment_method.supports?(source)
|
||||
invalidate!
|
||||
raise Core::GatewayError, Spree.t(:payment_method_not_supported)
|
||||
end
|
||||
return unless validate!
|
||||
|
||||
if payment_method.auto_capture?
|
||||
purchase!
|
||||
@@ -22,6 +13,16 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
def process_offline!
|
||||
return unless validate!
|
||||
|
||||
if payment_method.auto_capture?
|
||||
charge_offline!
|
||||
else
|
||||
authorize!
|
||||
end
|
||||
end
|
||||
|
||||
def authorize!
|
||||
started_processing!
|
||||
gateway_action(source, :authorize, :pend)
|
||||
@@ -32,6 +33,11 @@ module Spree
|
||||
gateway_action(source, :purchase, :complete)
|
||||
end
|
||||
|
||||
def charge_offline!
|
||||
started_processing!
|
||||
gateway_action(source, :charge_offline, :complete)
|
||||
end
|
||||
|
||||
def capture!
|
||||
return true if completed?
|
||||
|
||||
@@ -193,6 +199,20 @@ module Spree
|
||||
|
||||
private
|
||||
|
||||
def validate!
|
||||
return false unless payment_method&.source_required?
|
||||
|
||||
raise Core::GatewayError, Spree.t(:payment_processing_failed) unless source
|
||||
|
||||
return false if processing?
|
||||
|
||||
unless payment_method.supports?(source)
|
||||
invalidate!
|
||||
raise Core::GatewayError, Spree.t(:payment_method_not_supported)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def calculate_refund_amount(refund_amount = nil)
|
||||
refund_amount ||= if credit_allowed >= order.outstanding_balance.abs
|
||||
order.outstanding_balance.abs
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
module Api
|
||||
class CustomerSerializer < ActiveModel::Serializer
|
||||
attributes :id, :enterprise_id, :name, :code, :email, :allow_charges
|
||||
|
||||
def attributes
|
||||
hash = super
|
||||
if secret = object.gateway_recurring_payment_client_secret
|
||||
hash.merge!(gateway_recurring_payment_client_secret: secret)
|
||||
end
|
||||
hash.merge!(gateway_shop_id: object.gateway_shop_id) if object.gateway_shop_id
|
||||
hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
16
app/services/recurring_payments.rb
Normal file
16
app/services/recurring_payments.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RecurringPayments
|
||||
def self.setup_for(customer)
|
||||
return unless card = customer.user.default_card
|
||||
return unless stripe_account = customer.enterprise.stripe_account&.stripe_user_id
|
||||
|
||||
customer_id, payment_method_id =
|
||||
Stripe::CreditCardCloner.new(card, stripe_account).find_or_clone
|
||||
setup_intent = Stripe::SetupIntent.create(
|
||||
{ payment_method: payment_method_id, customer: customer_id },
|
||||
stripe_account: stripe_account
|
||||
)
|
||||
setup_intent.client_secret
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@
|
||||
- if Stripe.publishable_key
|
||||
:javascript
|
||||
angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
|
||||
angular.module('Darkswarm').value("stripePublishableKey", "#{Stripe.publishable_key}")
|
||||
|
||||
.darkswarm
|
||||
.row.pad-top
|
||||
|
||||
@@ -2511,6 +2511,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
js:
|
||||
saving: 'Saving...'
|
||||
changes_saved: 'Changes saved.'
|
||||
authorising: "Authorising..."
|
||||
save_changes_first: Save changes first.
|
||||
all_changes_saved: All changes saved
|
||||
unsaved_changes: You have unsaved changes
|
||||
|
||||
24
lib/stripe/credit_card_clone_destroyer.rb
Normal file
24
lib/stripe/credit_card_clone_destroyer.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Here we destroy (on Stripe) any clones that we have created for a platform card.
|
||||
# See CreditCardCloner for details.
|
||||
|
||||
# This is useful when the platform card is deleted (and needs to happen before the
|
||||
# platform card is deleted on Stripe).
|
||||
|
||||
module Stripe
|
||||
class CreditCardCloneDestroyer
|
||||
def destroy_clones(card)
|
||||
card.user.customers.each do |customer|
|
||||
next unless stripe_account = customer.enterprise.stripe_account&.stripe_user_id
|
||||
|
||||
customer_id, _payment_method_id =
|
||||
Stripe::CreditCardCloneFinder.new(card, stripe_account).find_cloned_card
|
||||
next unless customer_id
|
||||
|
||||
customer = Stripe::Customer.retrieve(customer_id, stripe_account: stripe_account)
|
||||
customer&.delete unless customer.deleted?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/stripe/credit_card_clone_finder.rb
Normal file
35
lib/stripe/credit_card_clone_finder.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stripe
|
||||
class CreditCardCloneFinder
|
||||
def initialize(card, stripe_account)
|
||||
@card = card
|
||||
@stripe_account = stripe_account
|
||||
end
|
||||
|
||||
def find_cloned_card
|
||||
return nil unless fingerprint = fingerprint_for_card(@card) && email = @card.user&.email
|
||||
|
||||
customers = Stripe::Customer.list({ email: email, limit: 100 }, stripe_account: @stripe_account)
|
||||
|
||||
customers.auto_paging_each do |customer|
|
||||
options = { customer: customer.id, type: 'card', limit: 100 }
|
||||
payment_methods = Stripe::PaymentMethod.list(options, stripe_account: @stripe_account)
|
||||
payment_methods.auto_paging_each do |payment_method|
|
||||
return [customer.id, payment_method.id] if clone?(payment_method, fingerprint)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clone?(payment_method, fingerprint)
|
||||
payment_method.card.fingerprint == fingerprint && payment_method.metadata["ofn-clone"]
|
||||
end
|
||||
|
||||
def fingerprint_for_card(card)
|
||||
Stripe::PaymentMethod.retrieve(card.gateway_payment_profile_id).card.fingerprint
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,52 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Here we clone
|
||||
# - a card (card_*) or payment_method (pm_*) stored (in a customer) in a platform account
|
||||
# into
|
||||
# Here we clone (or find a clone of)
|
||||
# - a card (card_*) or payment_method (pm_*) stored (in a customer) in a platform account into
|
||||
# - a payment method (pm_*) (in a new customer) in a connected account
|
||||
#
|
||||
# This is required when using the Stripe Payment Intents API:
|
||||
# - the customer and payment methods are stored in the platform account
|
||||
# so that they can be re-used across multiple sellers
|
||||
# - when a card needs to be charged, we need to create it in the seller's stripe account
|
||||
# - when a card needs to be charged, we need to clone (or find the clone)
|
||||
# in the seller's stripe account
|
||||
#
|
||||
# We are doing this process every time the card is charged:
|
||||
# - this means that, if the customer uses the same card on the same seller multiple times,
|
||||
# the card will be created multiple times on the seller's account
|
||||
# - to avoid this, we would have to store the IDs of every card on each seller's stripe account
|
||||
# in our database (this way we only have to store the platform account ID)
|
||||
# To avoid creating a new clone of the card/customer each time the card is charged or
|
||||
# authorized (e.g. for SCA), we attach metadata { clone: true } to the card the first time we
|
||||
# clone it and look for a card with the same fingerprint (hash of the card number) and
|
||||
# that metadata key to avoid cloning it multiple times.
|
||||
|
||||
require 'stripe/credit_card_clone_finder'
|
||||
|
||||
module Stripe
|
||||
class CreditCardCloner
|
||||
def clone(credit_card, connected_account_id)
|
||||
new_payment_method = clone_payment_method(credit_card, connected_account_id)
|
||||
def initialize(card, stripe_account)
|
||||
@card = card
|
||||
@stripe_account = stripe_account
|
||||
end
|
||||
|
||||
# If no customer is given, it will clone the payment method only
|
||||
return nil, new_payment_method.id if credit_card.gateway_customer_profile_id.blank?
|
||||
|
||||
new_customer = Stripe::Customer.create({ email: credit_card.user.email },
|
||||
stripe_account: connected_account_id)
|
||||
attach_payment_method_to_customer(new_payment_method.id,
|
||||
new_customer.id,
|
||||
connected_account_id)
|
||||
|
||||
[new_customer.id, new_payment_method.id]
|
||||
def find_or_clone
|
||||
cloned_card = Stripe::CreditCardCloneFinder.new(@card, @stripe_account).find_cloned_card
|
||||
cloned_card || clone
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clone_payment_method(credit_card, connected_account_id)
|
||||
platform_acct_payment_method_id = credit_card.gateway_payment_profile_id
|
||||
customer_id = credit_card.gateway_customer_profile_id
|
||||
def clone
|
||||
new_payment_method = clone_payment_method
|
||||
|
||||
# If no customer is given, it will clone the payment method only
|
||||
return [nil, new_payment_method.id] if @card.gateway_customer_profile_id.blank?
|
||||
|
||||
new_customer = Stripe::Customer.create({ email: @card.user.email },
|
||||
stripe_account: @stripe_account)
|
||||
attach_payment_method_to_customer(new_payment_method.id,
|
||||
new_customer.id)
|
||||
|
||||
add_metadata_to_payment_method(new_payment_method.id)
|
||||
|
||||
[new_customer.id, new_payment_method.id]
|
||||
end
|
||||
|
||||
def clone_payment_method
|
||||
platform_acct_payment_method_id = @card.gateway_payment_profile_id
|
||||
customer_id = @card.gateway_customer_profile_id
|
||||
|
||||
Stripe::PaymentMethod.create({ customer: customer_id,
|
||||
payment_method: platform_acct_payment_method_id },
|
||||
stripe_account: connected_account_id)
|
||||
stripe_account: @stripe_account)
|
||||
end
|
||||
|
||||
def attach_payment_method_to_customer(payment_method_id, customer_id, connected_account_id)
|
||||
def attach_payment_method_to_customer(payment_method_id, customer_id)
|
||||
Stripe::PaymentMethod.attach(payment_method_id,
|
||||
{ customer: customer_id },
|
||||
stripe_account: connected_account_id)
|
||||
stripe_account: @stripe_account)
|
||||
end
|
||||
|
||||
def add_metadata_to_payment_method(payment_method_id)
|
||||
Stripe::PaymentMethod.update(payment_method_id,
|
||||
{ metadata: { "ofn-clone": true } },
|
||||
stripe_account: @stripe_account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,6 +188,26 @@ describe Spree::CreditCardsController, type: :controller do
|
||||
expect(flash[:success]).to eq I18n.t(:card_has_been_removed, number: "x-#{card.last_digits}")
|
||||
expect(response).to redirect_to spree.account_path(anchor: 'cards')
|
||||
end
|
||||
|
||||
context "the card is the default card and there are existing authorizations for the user" do
|
||||
before do
|
||||
card.update_attribute(:is_default, true)
|
||||
end
|
||||
let!(:customer1) { create(:customer, allow_charges: true) }
|
||||
let!(:customer2) { create(:customer, allow_charges: true) }
|
||||
|
||||
it "removes the authorizations" do
|
||||
customer1.user = card.user
|
||||
customer2.user = card.user
|
||||
customer1.save
|
||||
customer2.save
|
||||
expect(customer1.reload.allow_charges).to be true
|
||||
expect(customer2.reload.allow_charges).to be true
|
||||
spree_delete :destroy, params
|
||||
expect(customer1.reload.allow_charges).to be false
|
||||
expect(customer2.reload.allow_charges).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,9 @@ feature '
|
||||
before do
|
||||
stub_payment_methods_post_request
|
||||
stub_payment_intent_get_request
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
end
|
||||
|
||||
context "for a complete order" do
|
||||
|
||||
@@ -4,11 +4,14 @@ require 'spec_helper'
|
||||
|
||||
feature "Credit Cards", js: true do
|
||||
include AuthenticationHelper
|
||||
include StripeHelper
|
||||
include StripeStubs
|
||||
|
||||
describe "as a logged in user" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:customer) { create(:customer, user: user) }
|
||||
let!(:default_card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_AZNMJ', is_default: true) }
|
||||
let!(:non_default_card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_FDTG') }
|
||||
let!(:default_card) { create(:stored_credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_AZNMJ', is_default: true) }
|
||||
let!(:non_default_card) { create(:stored_credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_FDTG') }
|
||||
|
||||
around do |example|
|
||||
original_stripe_connect_enabled = Spree::Config[:stripe_connect_enabled]
|
||||
@@ -19,7 +22,7 @@ feature "Credit Cards", js: true do
|
||||
before do
|
||||
login_as user
|
||||
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_xxxx" }
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
|
||||
allow(Stripe).to receive(:publishable_key) { "some_token" }
|
||||
Spree::Config.set(stripe_connect_enabled: true)
|
||||
|
||||
@@ -28,6 +31,9 @@ feature "Credit Cards", js: true do
|
||||
|
||||
stub_request(:delete, "https://api.stripe.com/v1/customers/cus_AZNMJ").
|
||||
to_return(status: 200, body: JSON.generate(deleted: true, id: "cus_AZNMJ"))
|
||||
stub_retrieve_payment_method_request("card_1EY...")
|
||||
stub_list_customers_request(email: user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_AZNMJ", response: {})
|
||||
end
|
||||
|
||||
it "passes the smoke test" do
|
||||
|
||||
@@ -97,6 +97,12 @@ feature "Check out with Stripe", js: true do
|
||||
end
|
||||
|
||||
context "with guest checkout" do
|
||||
before do
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
end
|
||||
|
||||
context "when the card is accepted" do
|
||||
before do
|
||||
stub_payment_intents_post_request order: order
|
||||
@@ -205,7 +211,12 @@ feature "Check out with Stripe", js: true do
|
||||
|
||||
context "saving a card and re-using it" do
|
||||
before do
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A123", response: {})
|
||||
stub_payment_methods_post_request request: { payment_method: "pm_123", customer: "cus_A123" }, response: { pm_id: "pm_123" }
|
||||
stub_add_metadata_request(payment_method: "pm_123", response: {})
|
||||
stub_payment_intents_post_request order: order
|
||||
stub_successful_capture_request order: order
|
||||
stub_customers_post_request email: user.email
|
||||
|
||||
@@ -160,7 +160,7 @@ describe SubscriptionConfirmJob do
|
||||
|
||||
context "when an error occurs while processing the payment" do
|
||||
before do
|
||||
expect(payment).to receive(:process!).and_raise Spree::Core::GatewayError, "payment failure error"
|
||||
expect(payment).to receive(:process_offline!).and_raise Spree::Core::GatewayError, "payment failure error"
|
||||
end
|
||||
|
||||
it "sends a failed payment email" do
|
||||
@@ -176,7 +176,7 @@ describe SubscriptionConfirmJob do
|
||||
end
|
||||
|
||||
before do
|
||||
expect(payment).to receive(:process!) { true }
|
||||
expect(payment).to receive(:process_offline!) { true }
|
||||
expect(payment).to receive(:completed?) { true }
|
||||
end
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@ require 'stripe/credit_card_cloner'
|
||||
|
||||
module Stripe
|
||||
describe CreditCardCloner do
|
||||
describe "#clone" do
|
||||
describe "#find_or_clone" do
|
||||
include StripeStubs
|
||||
|
||||
let(:cloner) { Stripe::CreditCardCloner.new }
|
||||
let(:credit_card) { create(:credit_card, user: create(:user)) }
|
||||
let(:stripe_account_id) { "abc123" }
|
||||
|
||||
let(:cloner) { Stripe::CreditCardCloner.new(credit_card, stripe_account_id) }
|
||||
|
||||
let(:customer_id) { "cus_A123" }
|
||||
let(:payment_method_id) { "pm_1234" }
|
||||
let(:new_customer_id) { "cus_A456" }
|
||||
let(:new_payment_method_id) { "pm_456" }
|
||||
let(:stripe_account_id) { "acct_456" }
|
||||
let(:payment_method_response_mock) { { status: 200, body: payment_method_response_body } }
|
||||
|
||||
let(:credit_card) { create(:credit_card, user: create(:user)) }
|
||||
|
||||
let(:payment_method_response_body) {
|
||||
JSON.generate(id: new_payment_method_id)
|
||||
}
|
||||
@@ -29,6 +29,12 @@ module Stripe
|
||||
stub_customers_post_request email: credit_card.user.email,
|
||||
response: { customer_id: new_customer_id },
|
||||
stripe_account_header: true
|
||||
|
||||
stub_retrieve_payment_method_request(payment_method_id)
|
||||
stub_list_customers_request(email: credit_card.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
stub_add_metadata_request(payment_method: "pm_456", response: {})
|
||||
|
||||
stub_request(:post,
|
||||
"https://api.stripe.com/v1/payment_methods/#{new_payment_method_id}/attach")
|
||||
.with(body: { customer: new_customer_id },
|
||||
@@ -47,7 +53,7 @@ module Stripe
|
||||
end
|
||||
|
||||
it "clones the payment method only" do
|
||||
customer_id, payment_method_id = cloner.clone(credit_card, stripe_account_id)
|
||||
customer_id, payment_method_id = cloner.find_or_clone
|
||||
|
||||
expect(payment_method_id).to eq new_payment_method_id
|
||||
expect(customer_id).to eq nil
|
||||
@@ -65,7 +71,7 @@ module Stripe
|
||||
end
|
||||
|
||||
it "clones both the payment method and the customer" do
|
||||
customer_id, payment_method_id = cloner.clone(credit_card, stripe_account_id)
|
||||
customer_id, payment_method_id = cloner.find_or_clone
|
||||
|
||||
expect(payment_method_id).to eq new_payment_method_id
|
||||
expect(customer_id).to eq new_customer_id
|
||||
|
||||
@@ -8,7 +8,7 @@ module Stripe
|
||||
describe "#call" do
|
||||
let(:validator) { Stripe::PaymentIntentValidator.new }
|
||||
let(:payment_intent_id) { "pi_123" }
|
||||
let(:stripe_account_id) { "acct_456" }
|
||||
let(:stripe_account_id) { "abc123" }
|
||||
let(:payment_intent_response_mock) { { status: 200, body: payment_intent_response_body } }
|
||||
|
||||
before do
|
||||
|
||||
@@ -6,6 +6,8 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
include ShopWorkflow
|
||||
include AuthenticationHelper
|
||||
include OpenFoodNetwork::ApiHelper
|
||||
include StripeHelper
|
||||
include StripeStubs
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:enterprise) { create(:distributor_enterprise) }
|
||||
@@ -104,6 +106,11 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques
|
||||
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)
|
||||
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
stub_add_metadata_request(payment_method: "pm_456", response: {})
|
||||
end
|
||||
|
||||
context "when the user submits a new card and doesn't request that the card is saved for later" do
|
||||
|
||||
@@ -35,14 +35,47 @@ module StripeStubs
|
||||
.to_return(hub_payment_method_response_mock({ pm_id: "pm_123" }))
|
||||
end
|
||||
|
||||
def stub_retrieve_payment_method_request(payment_method_id = "pm_1234")
|
||||
stub_request(:get, "https://api.stripe.com/v1/payment_methods/#{payment_method_id}")
|
||||
.with(headers: { 'Authorization' => 'Bearer sk_test_12345' })
|
||||
.to_return(retrieve_payment_method_response_mock({}))
|
||||
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 = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header
|
||||
stub.to_return(customers_response_mock(response))
|
||||
end
|
||||
|
||||
def stub_list_customers_request(email:, response: {})
|
||||
stub = stub_request(:get, "https://api.stripe.com/v1/customers?email=#{email}&limit=100")
|
||||
stub = stub.with(
|
||||
headers: { 'Authorization' => 'Bearer sk_test_12345', 'Stripe-Account' => 'abc123' }
|
||||
)
|
||||
stub.to_return(list_customers_response_mock(response))
|
||||
end
|
||||
|
||||
def stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
stub = stub_request(
|
||||
:get, "https://api.stripe.com/v1/payment_methods?customer=#{customer}&limit=100&type=card"
|
||||
)
|
||||
stub = stub.with(
|
||||
headers: { 'Authorization' => 'Bearer sk_test_12345', 'Stripe-Account' => 'abc123' }
|
||||
)
|
||||
stub.to_return(get_customer_payment_methods_response_mock(response))
|
||||
end
|
||||
|
||||
def stub_add_metadata_request(payment_method: "pm_456", response: {})
|
||||
stub = stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{payment_method}")
|
||||
stub = stub.with(body: { metadata: { "ofn-clone": true } })
|
||||
stub = stub.with(
|
||||
headers: { 'Authorization' => 'Bearer sk_test_12345', 'Stripe-Account' => 'abc123' }
|
||||
)
|
||||
stub.to_return(add_metadata_response_mock(response))
|
||||
end
|
||||
|
||||
def stub_successful_capture_request(order:, response: {})
|
||||
stub_capture_request(order, payment_successful_capture_mock(response))
|
||||
end
|
||||
@@ -119,4 +152,30 @@ module StripeStubs
|
||||
amount: 2000,
|
||||
charge: "ch_1234") }
|
||||
end
|
||||
|
||||
def retrieve_payment_method_response_mock(options)
|
||||
{ status: options[:code] || 200,
|
||||
body: JSON.generate(
|
||||
id: options[:pm_id] || "pm_456", customer: "cus_A123", card: { fingerprint: "12345" }
|
||||
) }
|
||||
end
|
||||
|
||||
def list_customers_response_mock(options)
|
||||
{ status: options[:code] || 200,
|
||||
body: JSON.generate(has_more: false, data: [{ id: "cus_A456" }]) }
|
||||
end
|
||||
|
||||
def get_customer_payment_methods_response_mock(options)
|
||||
payment_method = options[:payment_method] || "pm_456"
|
||||
fingerprint = options[:fingerprint] || "7890"
|
||||
{ status: options[:code] || 200,
|
||||
body: JSON.generate(
|
||||
has_more: false, data: [{ id: payment_method, card: { fingerprint: fingerprint } }]
|
||||
) }
|
||||
end
|
||||
|
||||
def add_metadata_response_mock(options)
|
||||
{ status: options[:code] || 200,
|
||||
body: JSON.generate({}) }
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user