mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-06 22:36:07 +00:00
Merge pull request #4672 from luisramos0/stripe_sca_method
Add new Stripe payment method compatible with the new Stripe Payment Intents API
This commit is contained in:
@@ -412,6 +412,8 @@ Metrics/AbcSize:
|
||||
- app/services/create_order_cycle.rb
|
||||
- app/services/order_syncer.rb
|
||||
- app/services/subscription_validator.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_decorator.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/customers_report.rb
|
||||
@@ -505,6 +507,7 @@ Metrics/CyclomaticComplexity:
|
||||
- app/models/spree/product_decorator.rb
|
||||
- app/models/variant_override_set.rb
|
||||
- app/services/cart_service.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/enterprise_issue_validator.rb
|
||||
@@ -530,6 +533,7 @@ Metrics/PerceivedComplexity:
|
||||
- app/models/spree/ability_decorator.rb
|
||||
- app/models/spree/order_decorator.rb
|
||||
- app/models/spree/product_decorator.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/enterprise_issue_validator.rb
|
||||
@@ -599,6 +603,7 @@ Metrics/MethodLength:
|
||||
- app/serializers/api/cached_enterprise_serializer.rb
|
||||
- app/services/order_cycle_form.rb
|
||||
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/column_preference_defaults.rb
|
||||
@@ -662,6 +667,7 @@ Metrics/ClassLength:
|
||||
- app/serializers/api/enterprise_shopfront_serializer.rb
|
||||
- app/services/cart_service.rb
|
||||
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
|
||||
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/enterprise_fee_calculator.rb
|
||||
- lib/open_food_network/order_cycle_form_applicator.rb
|
||||
|
||||
@@ -21,7 +21,7 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren
|
||||
year: @form_data.card_year
|
||||
verification_value: @form_data.card_verification_value
|
||||
}
|
||||
when 'stripe'
|
||||
when 'stripe', 'stripe_sca'
|
||||
angular.extend munged_payment.payment, {
|
||||
source_attributes:
|
||||
gateway_payment_profile_id: @form_data.token
|
||||
@@ -35,6 +35,8 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren
|
||||
purchase: ->
|
||||
if @paymentMethodType() == 'stripe'
|
||||
AdminStripeElements.requestToken(@form_data, @submit)
|
||||
else if @paymentMethodType() == 'stripe_sca'
|
||||
AdminStripeElements.createPaymentMethod(@form_data, @submit)
|
||||
else
|
||||
@submit()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ angular.module("admin.payments").factory 'AdminStripeElements', ($rootScope, Sta
|
||||
stripe: null
|
||||
card: null
|
||||
|
||||
# New Stripe Elements method
|
||||
# Create Token to be used with the Stripe Charges API
|
||||
requestToken: (secrets, submit) ->
|
||||
return unless @stripe? && @card?
|
||||
|
||||
@@ -20,6 +20,21 @@ angular.module("admin.payments").factory 'AdminStripeElements', ($rootScope, Sta
|
||||
secrets.card = response.token.card
|
||||
submit()
|
||||
|
||||
# Create Payment Method to be used with the Stripe Payment Intents API
|
||||
createPaymentMethod: (secrets, submit) ->
|
||||
return unless @stripe? && @card?
|
||||
|
||||
cardData = @makeCardData(secrets)
|
||||
|
||||
@stripe.createPaymentMethod({ type: 'card', card: @card }, @card, cardData).then (response) =>
|
||||
if(response.error)
|
||||
StatusMessage.display 'error', response.error.message
|
||||
else
|
||||
secrets.token = response.paymentMethod.id
|
||||
secrets.cc_type = response.paymentMethod.card.brand
|
||||
secrets.card = response.paymentMethod.card
|
||||
submit()
|
||||
|
||||
# Maps the brand returned by Stripe to that required by activemerchant
|
||||
mapCC: (ccType) ->
|
||||
switch ccType
|
||||
|
||||
@@ -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")
|
||||
$scope.cardRequired = (paymentMethod.type == "Spree::Gateway::StripeConnect" || paymentMethod.type == "Spree::Gateway::StripeSCA")
|
||||
$scope.loadCustomer() if $scope.cardRequired && !$scope.customer
|
||||
|
||||
$scope.loadCustomer = ->
|
||||
|
||||
@@ -7,6 +7,8 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE
|
||||
purchase: ->
|
||||
if @paymentMethod()?.method_type == 'stripe' && !@secrets.selected_card
|
||||
StripeElements.requestToken(@secrets, @submit)
|
||||
else if @paymentMethod()?.method_type == 'stripe_sca' && !@secrets.selected_card
|
||||
StripeElements.createPaymentMethod(@secrets, @submit)
|
||||
else
|
||||
@submit()
|
||||
|
||||
@@ -59,7 +61,7 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE
|
||||
last_name: @order.bill_address.lastname
|
||||
}
|
||||
|
||||
if @paymentMethod()?.method_type == 'stripe'
|
||||
if @paymentMethod()?.method_type == 'stripe' || @paymentMethod()?.method_type == 'stripe_sca'
|
||||
if @secrets.selected_card
|
||||
angular.extend munged_order, {
|
||||
existing_card_id: @secrets.selected_card
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
Darkswarm.factory 'StripeElements', ($rootScope, Loading, RailsFlashLoader) ->
|
||||
new class StripeElements
|
||||
# TODO: add locale here for translations of error messages etc. from Stripe
|
||||
|
||||
# These are both set from the StripeElements directive
|
||||
stripe: null
|
||||
card: null
|
||||
|
||||
# New Stripe Elements method
|
||||
# Create Token to be used with the Stripe Charges API
|
||||
requestToken: (secrets, submit, loading_message = t("processing_payment")) ->
|
||||
return unless @stripe? && @card?
|
||||
|
||||
@@ -23,6 +21,23 @@ Darkswarm.factory 'StripeElements', ($rootScope, Loading, RailsFlashLoader) ->
|
||||
secrets.card = response.token.card
|
||||
submit()
|
||||
|
||||
# Create Payment Method to be used with the Stripe Payment Intents API
|
||||
createPaymentMethod: (secrets, submit, loading_message = t("processing_payment")) ->
|
||||
return unless @stripe? && @card?
|
||||
|
||||
Loading.message = loading_message
|
||||
cardData = @makeCardData(secrets)
|
||||
|
||||
@stripe.createPaymentMethod({ type: 'card', card: @card }, @card, cardData).then (response) =>
|
||||
if(response.error)
|
||||
Loading.clear()
|
||||
RailsFlashLoader.loadFlash({error: t("error") + ": #{response.error.message}"})
|
||||
else
|
||||
secrets.token = response.paymentMethod.id
|
||||
secrets.cc_type = response.paymentMethod.card.brand
|
||||
secrets.card = response.paymentMethod.card
|
||||
submit()
|
||||
|
||||
# Maps the brand returned by Stripe to that required by activemerchant
|
||||
mapCC: (ccType) ->
|
||||
switch ccType
|
||||
|
||||
@@ -110,7 +110,7 @@ module Spree
|
||||
else
|
||||
Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort_by(&:name)
|
||||
end
|
||||
@providers.reject!{ |p| p.name.ends_with? "StripeConnect" } unless show_stripe?
|
||||
@providers.reject!{ |provider| stripe_provider?(provider) } unless show_stripe?
|
||||
@calculators = PaymentMethod.calculators.sort_by(&:name)
|
||||
end
|
||||
|
||||
@@ -134,12 +134,12 @@ module Spree
|
||||
# current payment_method is already a Stripe method
|
||||
def show_stripe?
|
||||
Spree::Config.stripe_connect_enabled ||
|
||||
@payment_method.try(:type) == "Spree::Gateway::StripeConnect"
|
||||
stripe_payment_method?
|
||||
end
|
||||
|
||||
def restrict_stripe_account_change
|
||||
return unless @payment_method
|
||||
return unless @payment_method.type == "Spree::Gateway::StripeConnect"
|
||||
return unless stripe_payment_method?
|
||||
return unless @payment_method.preferred_enterprise_id.andand > 0
|
||||
|
||||
@stripe_account_holder = Enterprise.find(@payment_method.preferred_enterprise_id)
|
||||
@@ -147,6 +147,15 @@ module Spree
|
||||
|
||||
params[:payment_method][:preferred_enterprise_id] = @stripe_account_holder.id
|
||||
end
|
||||
|
||||
def stripe_payment_method?
|
||||
["Spree::Gateway::StripeConnect",
|
||||
"Spree::Gateway::StripeSCA"].include? @payment_method.try(:type)
|
||||
end
|
||||
|
||||
def stripe_provider?(provider)
|
||||
provider.name.ends_with?("StripeConnect", "StripeSCA")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,10 +10,14 @@ module Spree
|
||||
render json: @credit_card, serializer: ::Api::CreditCardSerializer, status: :ok
|
||||
else
|
||||
message = t(:card_could_not_be_saved)
|
||||
render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: message) } }, status: :bad_request
|
||||
render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout,
|
||||
error: message) } },
|
||||
status: :bad_request
|
||||
end
|
||||
rescue Stripe::CardError => e
|
||||
render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: e.message) } }, status: :bad_request
|
||||
render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout,
|
||||
error: e.message) } },
|
||||
status: :bad_request
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -54,10 +58,23 @@ module Spree
|
||||
|
||||
# Currently can only destroy the whole customer object
|
||||
def destroy_at_stripe
|
||||
stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id)
|
||||
if @credit_card.payment_method &&
|
||||
@credit_card.payment_method.type == "Spree::Gateway::StripeSCA"
|
||||
options = { stripe_account: stripe_account_id }
|
||||
end
|
||||
|
||||
stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id,
|
||||
options || {})
|
||||
stripe_customer.delete if stripe_customer
|
||||
end
|
||||
|
||||
def stripe_account_id
|
||||
StripeAccount.
|
||||
find_by_enterprise_id(@credit_card.payment_method.preferred_enterprise_id).
|
||||
andand.
|
||||
stripe_user_id
|
||||
end
|
||||
|
||||
def create_customer(token)
|
||||
Stripe::Customer.create(email: spree_current_user.email, source: token)
|
||||
end
|
||||
|
||||
@@ -9,12 +9,6 @@ module Spree
|
||||
|
||||
attr_accessible :preferred_enterprise_id
|
||||
|
||||
CARD_TYPE_MAPPING = {
|
||||
'American Express' => 'american_express',
|
||||
'Diners Club' => 'diners_club',
|
||||
'Visa' => 'visa'
|
||||
}.freeze
|
||||
|
||||
def method_type
|
||||
'stripe'
|
||||
end
|
||||
@@ -77,11 +71,6 @@ module Spree
|
||||
[money, creditcard, options]
|
||||
end
|
||||
|
||||
def update_source!(source)
|
||||
source.cc_type = CARD_TYPE_MAPPING[source.cc_type] if CARD_TYPE_MAPPING.include?(source.cc_type)
|
||||
source
|
||||
end
|
||||
|
||||
def token_from_card_profile_ids(creditcard)
|
||||
token_or_card_id = creditcard.gateway_payment_profile_id
|
||||
customer = creditcard.gateway_customer_profile_id
|
||||
|
||||
90
app/models/spree/gateway/stripe_sca.rb
Normal file
90
app/models/spree/gateway/stripe_sca.rb
Normal file
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'stripe/profile_storer'
|
||||
require 'active_merchant/billing/gateways/stripe_payment_intents'
|
||||
require 'active_merchant/billing/gateways/stripe_decorator'
|
||||
|
||||
module Spree
|
||||
class Gateway
|
||||
class StripeSCA < Gateway
|
||||
preference :enterprise_id, :integer
|
||||
|
||||
validate :ensure_enterprise_selected
|
||||
|
||||
attr_accessible :preferred_enterprise_id
|
||||
|
||||
def method_type
|
||||
'stripe_sca'
|
||||
end
|
||||
|
||||
def provider_class
|
||||
ActiveMerchant::Billing::StripePaymentIntentsGateway
|
||||
end
|
||||
|
||||
def payment_profiles_supported?
|
||||
true
|
||||
end
|
||||
|
||||
def stripe_account_id
|
||||
StripeAccount.find_by_enterprise_id(preferred_enterprise_id).andand.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
|
||||
|
||||
# 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, stripe_account_id)
|
||||
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
|
||||
|
||||
options[:customer] = creditcard.gateway_customer_profile_id
|
||||
creditcard = creditcard.gateway_payment_profile_id
|
||||
|
||||
[money, creditcard, options]
|
||||
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.andand.positive?
|
||||
|
||||
errors.add(:stripe_account_owner, I18n.t(:error_required))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -68,6 +68,8 @@ Spree::PaymentMethod.class_eval do
|
||||
"Pin Payments"
|
||||
when "Spree::Gateway::StripeConnect"
|
||||
"Stripe"
|
||||
when "Spree::Gateway::StripeSCA"
|
||||
"Stripe SCA"
|
||||
when "Spree::Gateway::PayPalExpress"
|
||||
"PayPal Express"
|
||||
else
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class Subscription < ActiveRecord::Base
|
||||
ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check", "Spree::Gateway::StripeConnect"].freeze
|
||||
ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check",
|
||||
"Spree::Gateway::StripeConnect",
|
||||
"Spree::Gateway::StripeSCA"].freeze
|
||||
|
||||
belongs_to :shop, class_name: 'Enterprise'
|
||||
belongs_to :customer
|
||||
|
||||
@@ -4,7 +4,8 @@ module Api
|
||||
delegate :serializable_hash, to: :method_serializer
|
||||
|
||||
def method_serializer
|
||||
if object.type == 'Spree::Gateway::StripeConnect'
|
||||
if object.type == 'Spree::Gateway::StripeConnect' ||
|
||||
object.type == 'Spree::Gateway::StripeSCA'
|
||||
Api::Admin::PaymentMethod::StripeSerializer.new(object)
|
||||
else
|
||||
Api::Admin::PaymentMethod::BaseSerializer.new(object)
|
||||
|
||||
@@ -82,13 +82,18 @@ class SubscriptionValidator
|
||||
|
||||
def credit_card_ok?
|
||||
return unless customer && payment_method
|
||||
return unless payment_method.type == "Spree::Gateway::StripeConnect"
|
||||
return unless stripe_payment_method?(payment_method)
|
||||
return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges
|
||||
return if customer.user.andand.default_card.present?
|
||||
|
||||
errors.add(:payment_method, :no_default_card)
|
||||
end
|
||||
|
||||
def stripe_payment_method?(payment_method)
|
||||
payment_method.type == "Spree::Gateway::StripeConnect" ||
|
||||
payment_method.type == "Spree::Gateway::StripeSCA"
|
||||
end
|
||||
|
||||
def subscription_line_items_present?
|
||||
return if subscription_line_items.reject(&:marked_for_destruction?).any?
|
||||
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
- content_for :injection_data do
|
||||
- if Stripe.publishable_key
|
||||
:javascript
|
||||
angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
|
||||
|
||||
%fieldset#payment
|
||||
%ng-form{"ng-controller" => "PaymentCtrl", name: "payment"}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
= @payment_method
|
||||
- case @payment_method
|
||||
- when Spree::Gateway::StripeConnect
|
||||
= render 'stripe_connect'
|
||||
- when Spree::Gateway::StripeSCA
|
||||
= render 'stripe_connect'
|
||||
- else
|
||||
- if @payment_method.preferences.present?
|
||||
%fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.stripe
|
||||
%script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"}
|
||||
- if Stripe.publishable_key
|
||||
:javascript
|
||||
angular.module('admin.payments').value("stripeObject", Stripe("#{Stripe.publishable_key}", { stripeAccount: "#{StripeAccount.find_by_enterprise_id(payment_method.preferred_enterprise_id).andand.stripe_user_id}" }))
|
||||
|
||||
.row
|
||||
.three.columns
|
||||
= label_tag :cardholder_name, t(:cardholder_name)
|
||||
.six.columns
|
||||
= text_field_tag :cardholder_name, nil, {size: 40, "ng-model" => 'form_data.name'}
|
||||
.row
|
||||
.three.columns
|
||||
= label_tag :card_details, t(:card_details)
|
||||
.six.columns
|
||||
%stripe-elements
|
||||
@@ -1,3 +1,8 @@
|
||||
- content_for :injection_data do
|
||||
- if Stripe.publishable_key
|
||||
:javascript
|
||||
angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
|
||||
|
||||
.row{ "ng-show" => "savedCreditCards.length > 0" }
|
||||
.small-12.columns
|
||||
%h6= t('.used_saved_card')
|
||||
|
||||
22
app/views/spree/checkout/payment/_stripe_sca.html.haml
Normal file
22
app/views/spree/checkout/payment/_stripe_sca.html.haml
Normal file
@@ -0,0 +1,22 @@
|
||||
- content_for :injection_data do
|
||||
- if Stripe.publishable_key
|
||||
:javascript
|
||||
angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}", { stripeAccount: "#{StripeAccount.find_by_enterprise_id(payment_method.preferred_enterprise_id).andand.stripe_user_id}" }))
|
||||
|
||||
.row{ "ng-show" => "savedCreditCards.length > 0" }
|
||||
.small-12.columns
|
||||
%h6= t('.used_saved_card')
|
||||
%select{ name: "selected_card", required: false, ng: { model: "secrets.selected_card", options: "card.id as card.formatted for card in savedCreditCards" } }
|
||||
%option{ value: "" }= "{{ secrets.selected_card ? '#{t('.enter_new_card')}' : '#{t('.choose_one')}' }}"
|
||||
|
||||
%h6{ ng: { if: '!secrets.selected_card' } }
|
||||
= t('.or_enter_new_card')
|
||||
|
||||
%div{ ng: { if: '!secrets.selected_card' } }
|
||||
%stripe-elements
|
||||
|
||||
- if spree_current_user
|
||||
.row
|
||||
.small-12.columns.text-right
|
||||
= check_box_tag 'secrets.save_requested_by_customer', '1', false, 'ng-model' => 'secrets.save_requested_by_customer'
|
||||
= label_tag 'secrets.save_requested_by_customer', t('.remember_this_card')
|
||||
@@ -92,6 +92,7 @@ module Openfoodnetwork
|
||||
app.config.spree.payment_methods << Spree::Gateway::Migs
|
||||
app.config.spree.payment_methods << Spree::Gateway::Pin
|
||||
app.config.spree.payment_methods << Spree::Gateway::StripeConnect
|
||||
app.config.spree.payment_methods << Spree::Gateway::StripeSCA
|
||||
end
|
||||
|
||||
# Settings in config/environments/* take precedence over those specified here.
|
||||
|
||||
@@ -3383,6 +3383,12 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
used_saved_card: "Use a saved card:"
|
||||
or_enter_new_card: "Or, enter details for a new card:"
|
||||
remember_this_card: Remember this card?
|
||||
stripe_sca:
|
||||
choose_one: Choose one
|
||||
enter_new_card: Enter details for a new card
|
||||
used_saved_card: "Use a saved card:"
|
||||
or_enter_new_card: "Or, enter details for a new card:"
|
||||
remember_this_card: Remember this card?
|
||||
date_picker:
|
||||
format: ! '%Y-%m-%d'
|
||||
js_format: 'yy-mm-dd'
|
||||
|
||||
19
lib/active_merchant/billing/gateways/stripe_decorator.rb
Normal file
19
lib/active_merchant/billing/gateways/stripe_decorator.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Here we bring commit 823faaeab0d6d3bd75ee037ec894ab7c9d95d3a9 from ActiveMerchant v1.98.0
|
||||
# This is needed to make StripePaymentIntents work correctly
|
||||
# This can be removed once we upgrade to ActiveMerchant v1.98.0
|
||||
ActiveMerchant::Billing::StripeGateway.class_eval do
|
||||
def authorization_from(success, url, method, response)
|
||||
return response.fetch('error', {})['charge'] unless success
|
||||
|
||||
if url == 'customers'
|
||||
[response['id'], response.dig('sources', 'data').first&.dig('id')].join('|')
|
||||
elsif method == :post &&
|
||||
(url.match(%r{customers/.*/cards}) || url.match(%r{payment_methods/.*/attach}))
|
||||
[response['customer'], response['id']].join('|')
|
||||
else
|
||||
response['id']
|
||||
end
|
||||
end
|
||||
end
|
||||
277
lib/active_merchant/billing/gateways/stripe_payment_intents.rb
Normal file
277
lib/active_merchant/billing/gateways/stripe_payment_intents.rb
Normal file
@@ -0,0 +1,277 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Here we bring commit 823faaeab0d6d3bd75ee037ec894ab7c9d95d3a9 from ActiveMerchant v1.98.0
|
||||
# This class integrates with the new StripePaymentIntents API
|
||||
# This can be removed once we upgrade to ActiveMerchant v1.98.0
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
|
||||
module ActiveMerchant #:nodoc:
|
||||
module Billing #:nodoc:
|
||||
# This gateway uses the current Stripe
|
||||
# {Payment Intents API}[https://stripe.com/docs/api/payment_intents].
|
||||
# For the legacy API, see the Stripe gateway
|
||||
class StripePaymentIntentsGateway < StripeGateway
|
||||
ALLOWED_METHOD_STATES = %w[automatic manual].freeze
|
||||
ALLOWED_CANCELLATION_REASONS = %w[duplicate fraudulent requested_by_customer abandoned].freeze
|
||||
CREATE_INTENT_ATTRIBUTES =
|
||||
%i[description statement_descriptor receipt_email save_payment_method].freeze
|
||||
CONFIRM_INTENT_ATTRIBUTES =
|
||||
%i[receipt_email return_url save_payment_method setup_future_usage off_session].freeze
|
||||
UPDATE_INTENT_ATTRIBUTES =
|
||||
%i[description statement_descriptor receipt_email setup_future_usage].freeze
|
||||
DEFAULT_API_VERSION = '2019-05-16'
|
||||
|
||||
def create_intent(money, payment_method, options = {})
|
||||
post = {}
|
||||
add_amount(post, money, options, true)
|
||||
add_capture_method(post, options)
|
||||
add_confirmation_method(post, options)
|
||||
add_customer(post, options)
|
||||
add_payment_method_token(post, payment_method, options)
|
||||
add_metadata(post, options)
|
||||
add_return_url(post, options)
|
||||
add_connected_account(post, options)
|
||||
add_shipping_address(post, options)
|
||||
setup_future_usage(post, options)
|
||||
|
||||
CREATE_INTENT_ATTRIBUTES.each do |attribute|
|
||||
add_whitelisted_attribute(post, options, attribute)
|
||||
end
|
||||
|
||||
commit(:post, 'payment_intents', post, options)
|
||||
end
|
||||
|
||||
def show_intent(intent_id, options)
|
||||
commit(:get, "payment_intents/#{intent_id}", nil, options)
|
||||
end
|
||||
|
||||
def confirm_intent(intent_id, payment_method, options = {})
|
||||
post = {}
|
||||
add_payment_method_token(post, payment_method, options)
|
||||
CONFIRM_INTENT_ATTRIBUTES.each do |attribute|
|
||||
add_whitelisted_attribute(post, options, attribute)
|
||||
end
|
||||
|
||||
commit(:post, "payment_intents/#{intent_id}/confirm", post, options)
|
||||
end
|
||||
|
||||
def create_payment_method(payment_method, options = {})
|
||||
post = {}
|
||||
post[:type] = 'card'
|
||||
post[:card] = {}
|
||||
post[:card][:number] = payment_method.number
|
||||
post[:card][:exp_month] = payment_method.month
|
||||
post[:card][:exp_year] = payment_method.year
|
||||
post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
|
||||
|
||||
commit(:post, 'payment_methods', post, options)
|
||||
end
|
||||
|
||||
def update_intent(money, intent_id, payment_method, options = {})
|
||||
post = {}
|
||||
post[:amount] = money if money
|
||||
|
||||
add_payment_method_token(post, payment_method, options)
|
||||
add_payment_method_types(post, options)
|
||||
add_customer(post, options)
|
||||
add_metadata(post, options)
|
||||
add_shipping_address(post, options)
|
||||
add_connected_account(post, options)
|
||||
|
||||
UPDATE_INTENT_ATTRIBUTES.each do |attribute|
|
||||
add_whitelisted_attribute(post, options, attribute)
|
||||
end
|
||||
|
||||
commit(:post, "payment_intents/#{intent_id}", post, options)
|
||||
end
|
||||
|
||||
def authorize(money, payment_method, options = {})
|
||||
create_intent(money,
|
||||
payment_method,
|
||||
options.merge!(confirm: true, capture_method: 'manual'))
|
||||
end
|
||||
|
||||
def purchase(money, payment_method, options = {})
|
||||
create_intent(money,
|
||||
payment_method,
|
||||
options.merge!(confirm: true, capture_method: 'automatic'))
|
||||
end
|
||||
|
||||
def capture(money, intent_id, options = {})
|
||||
post = {}
|
||||
post[:amount_to_capture] = money
|
||||
add_connected_account(post, options)
|
||||
commit(:post, "payment_intents/#{intent_id}/capture", post, options)
|
||||
end
|
||||
|
||||
def void(intent_id, options = {})
|
||||
post = {}
|
||||
if ALLOWED_CANCELLATION_REASONS.include?(options[:cancellation_reason])
|
||||
post[:cancellation_reason] = options[:cancellation_reason]
|
||||
end
|
||||
commit(:post, "payment_intents/#{intent_id}/cancel", post, options)
|
||||
end
|
||||
|
||||
def refund(money, intent_id, options = {})
|
||||
intent = commit(:get, "payment_intents/#{intent_id}", nil, options)
|
||||
charge_id = intent.params.dig('charges', 'data')[0].dig('id')
|
||||
super(money, charge_id, options)
|
||||
end
|
||||
|
||||
# Note: Not all payment methods are currently supported by the
|
||||
# {Payment Methods API}[https://stripe.com/docs/payments/payment-methods]
|
||||
# Current implementation will create
|
||||
# a PaymentMethod object if the method is a token or credit card
|
||||
# All other types will default to legacy Stripe store
|
||||
def store(payment_method, options = {})
|
||||
params = {}
|
||||
post = {}
|
||||
|
||||
# If customer option is provided, create a payment method and attach to customer id
|
||||
# Otherwise, create a customer, then attach
|
||||
# if payment_method.is_a?(StripePaymentToken) ||
|
||||
# payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
|
||||
add_payment_method_token(params, payment_method, options)
|
||||
if options[:customer]
|
||||
customer_id = options[:customer]
|
||||
else
|
||||
post[:validate] = options[:validate] unless options[:validate].nil?
|
||||
post[:description] = options[:description] if options[:description]
|
||||
post[:email] = options[:email] if options[:email]
|
||||
customer = commit(:post, 'customers', post, options)
|
||||
customer_id = customer.params['id']
|
||||
|
||||
# return the stripe response if expected customer id is not present
|
||||
return customer if customer_id.nil?
|
||||
end
|
||||
commit(:post,
|
||||
"payment_methods/#{params[:payment_method]}/attach",
|
||||
{ customer: customer_id }, options)
|
||||
# else
|
||||
# super(payment, options)
|
||||
# end
|
||||
end
|
||||
|
||||
def unstore(identification, options = {}, deprecated_options = {})
|
||||
if identification.include?('pm_')
|
||||
_, payment_method = identification.split('|')
|
||||
commit(:post, "payment_methods/#{payment_method}/detach", nil, options)
|
||||
else
|
||||
super(identification, options, deprecated_options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_whitelisted_attribute(post, options, attribute)
|
||||
post[attribute] = options[attribute] if options[attribute]
|
||||
post
|
||||
end
|
||||
|
||||
def add_capture_method(post, options)
|
||||
capture_method = options[:capture_method].to_s
|
||||
post[:capture_method] = capture_method if ALLOWED_METHOD_STATES.include?(capture_method)
|
||||
post
|
||||
end
|
||||
|
||||
def add_confirmation_method(post, options)
|
||||
confirmation_method = options[:confirmation_method].to_s
|
||||
if ALLOWED_METHOD_STATES.include?(confirmation_method)
|
||||
post[:confirmation_method] = confirmation_method
|
||||
end
|
||||
post
|
||||
end
|
||||
|
||||
def add_customer(post, options)
|
||||
customer = options[:customer].to_s
|
||||
post[:customer] = customer if customer.start_with?('cus_')
|
||||
post
|
||||
end
|
||||
|
||||
def add_return_url(post, options)
|
||||
return unless options[:confirm]
|
||||
|
||||
post[:confirm] = options[:confirm]
|
||||
post[:return_url] = options[:return_url] if options[:return_url]
|
||||
post
|
||||
end
|
||||
|
||||
def add_payment_method_token(post, payment_method, options)
|
||||
return if payment_method.nil?
|
||||
|
||||
if payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
|
||||
p = create_payment_method(payment_method, options)
|
||||
payment_method = p.params['id']
|
||||
end
|
||||
|
||||
if payment_method.is_a?(StripePaymentToken)
|
||||
post[:payment_method] = payment_method.payment_data['id']
|
||||
elsif payment_method.is_a?(String)
|
||||
if payment_method.include?('|')
|
||||
customer_id, payment_method_id = payment_method.split('|')
|
||||
token = payment_method_id
|
||||
post[:customer] = customer_id
|
||||
else
|
||||
token = payment_method
|
||||
end
|
||||
post[:payment_method] = token
|
||||
end
|
||||
end
|
||||
|
||||
def add_payment_method_types(post, options)
|
||||
payment_method_types = options[:payment_method_types] if options[:payment_method_types]
|
||||
return if payment_method_types.nil?
|
||||
|
||||
post[:payment_method_types] = Array(payment_method_types)
|
||||
post
|
||||
end
|
||||
|
||||
def setup_future_usage(post, options = {})
|
||||
if %w(on_session off_session).include?(options[:setup_future_usage])
|
||||
post[:setup_future_usage] = options[:setup_future_usage]
|
||||
end
|
||||
if options[:off_session] && options[:confirm] == true
|
||||
post[:off_session] = options[:off_session]
|
||||
end
|
||||
post
|
||||
end
|
||||
|
||||
def add_connected_account(post, options = {})
|
||||
return unless transfer_data = options[:transfer_data]
|
||||
|
||||
post[:transfer_data] = {}
|
||||
if transfer_data[:destination]
|
||||
post[:transfer_data][:destination] = transfer_data[:destination]
|
||||
end
|
||||
post[:transfer_data][:amount] = transfer_data[:amount] if transfer_data[:amount]
|
||||
post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of]
|
||||
post[:transfer_group] = options[:transfer_group] if options[:transfer_group]
|
||||
post[:application_fee_amount] = options[:application_fee] if options[:application_fee]
|
||||
post
|
||||
end
|
||||
|
||||
def add_shipping_address(post, options = {})
|
||||
return unless shipping = options[:shipping]
|
||||
|
||||
post[:shipping] = {}
|
||||
post[:shipping][:address] = {}
|
||||
post[:shipping][:address][:line1] = shipping[:address][:line1]
|
||||
post[:shipping][:address][:city] = shipping[:address][:city] if shipping[:address][:city]
|
||||
if shipping[:address][:country]
|
||||
post[:shipping][:address][:country] = shipping[:address][:country]
|
||||
end
|
||||
post[:shipping][:address][:line2] = shipping[:address][:line2] if shipping[:address][:line2]
|
||||
if shipping[:address][:postal_code]
|
||||
post[:shipping][:address][:postal_code] = shipping[:address][:postal_code]
|
||||
end
|
||||
post[:shipping][:address][:state] = shipping[:address][:state] if shipping[:address][:state]
|
||||
|
||||
post[:shipping][:name] = shipping[:name]
|
||||
post[:shipping][:carrier] = shipping[:carrier] if shipping[:carrier]
|
||||
post[:shipping][:phone] = shipping[:phone] if shipping[:phone]
|
||||
post[:shipping][:tracking_number] = shipping[:tracking_number] if shipping[:tracking_number]
|
||||
post
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,8 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def card_required?
|
||||
payment.payment_method.is_a? Spree::Gateway::StripeConnect
|
||||
[Spree::Gateway::StripeConnect,
|
||||
Spree::Gateway::StripeSCA].include? payment.payment_method.class
|
||||
end
|
||||
|
||||
def card_set?
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
module Stripe
|
||||
class ProfileStorer
|
||||
def initialize(payment, provider)
|
||||
def initialize(payment, provider, stripe_account_id = nil)
|
||||
@payment = payment
|
||||
@provider = provider
|
||||
@stripe_account_id = stripe_account_id
|
||||
end
|
||||
|
||||
def create_customer_from_token
|
||||
@@ -28,7 +29,13 @@ module Stripe
|
||||
email: @payment.order.email,
|
||||
login: Stripe.api_key,
|
||||
address: address_for(@payment)
|
||||
}
|
||||
}.merge(stripe_account_option)
|
||||
end
|
||||
|
||||
def stripe_account_option
|
||||
return {} if @stripe_account_id.blank?
|
||||
|
||||
{ stripe_account: @stripe_account_id }
|
||||
end
|
||||
|
||||
def address_for(payment)
|
||||
@@ -54,10 +61,22 @@ module Stripe
|
||||
|
||||
def source_attrs_from(response)
|
||||
{
|
||||
cc_type: @payment.source.cc_type, # side-effect of update_source!
|
||||
gateway_customer_profile_id: response.params['id'],
|
||||
gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
|
||||
cc_type: @payment.source.cc_type,
|
||||
gateway_customer_profile_id: customer_profile_id(response),
|
||||
gateway_payment_profile_id: payment_profile_id(response)
|
||||
}
|
||||
end
|
||||
|
||||
def customer_profile_id(response)
|
||||
response.params['customer'] || response.params['id']
|
||||
end
|
||||
|
||||
def payment_profile_id(response)
|
||||
if response.params['customer'] # Payment Intents API
|
||||
response.params['id']
|
||||
else
|
||||
response.params['default_source'] || response.params['default_card']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,6 +172,22 @@ describe Spree::CreditCardsController, type: :controller do
|
||||
expect(response).to redirect_to spree.account_path(anchor: 'cards')
|
||||
end
|
||||
end
|
||||
|
||||
context "where the payment method is StripeSCA" do
|
||||
let(:stripe_payment_method) { create(:stripe_sca_payment_method) }
|
||||
let!(:card) { create(:credit_card, gateway_customer_profile_id: 'cus_AZNMJ', payment_method: stripe_payment_method) }
|
||||
|
||||
before do
|
||||
stub_request(:delete, "https://api.stripe.com/v1/customers/cus_AZNMJ").
|
||||
to_return(status: 200, body: JSON.generate(deleted: true, id: "cus_AZNMJ"))
|
||||
end
|
||||
|
||||
it "the request to destroy the Stripe customer includes the stripe_account_id" do
|
||||
expect(Stripe::Customer).to receive(:retrieve).with(card.gateway_customer_profile_id, { stripe_account: "abc123" })
|
||||
|
||||
expect{ delete :destroy, params }.to change(Spree::CreditCard, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -151,8 +151,15 @@ FactoryBot.define do
|
||||
preferred_enterprise_id { distributors.first.id }
|
||||
end
|
||||
|
||||
factory :stripe_sca_payment_method, class: Spree::Gateway::StripeSCA do
|
||||
name 'StripeSCA'
|
||||
environment 'test'
|
||||
distributors { [FactoryBot.create(:stripe_account).enterprise] }
|
||||
preferred_enterprise_id { distributors.first.id }
|
||||
end
|
||||
|
||||
factory :stripe_account do
|
||||
enterprise { FactoryBot.create :distributor_enterprise }
|
||||
enterprise { FactoryBot.create(:distributor_enterprise) }
|
||||
stripe_user_id "abc123"
|
||||
stripe_publishable_key "xyz456"
|
||||
end
|
||||
|
||||
52
spec/lib/stripe/profile_storer_spec.rb
Normal file
52
spec/lib/stripe/profile_storer_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Stripe
|
||||
describe ProfileStorer do
|
||||
describe "create_customer_from_token" do
|
||||
let(:payment) { create(:payment) }
|
||||
let(:stripe_payment_method) { create(:stripe_payment_method) }
|
||||
let(:stripe_account_id) { "12312" }
|
||||
let(:profile_storer) { Stripe::ProfileStorer.new(payment, stripe_payment_method.provider) }
|
||||
|
||||
let(:customer_id) { "cus_A123" }
|
||||
let(:card_id) { "card_2342" }
|
||||
let(:customer_response_mock) { { status: 200, body: customer_response_body } }
|
||||
|
||||
before do
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
|
||||
|
||||
stub_request(:post, "https://api.stripe.com/v1/customers")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: { email: payment.order.email })
|
||||
.to_return(customer_response_mock)
|
||||
end
|
||||
|
||||
context "when called from Stripe Connect" do
|
||||
let(:customer_response_body) {
|
||||
JSON.generate(id: customer_id, default_card: card_id, sources: { data: [{ id: "1" }] })
|
||||
}
|
||||
|
||||
it "fetches the customer id and the card id from the correct response fields" do
|
||||
profile_storer.create_customer_from_token
|
||||
|
||||
expect(payment.source.gateway_customer_profile_id).to eq customer_id
|
||||
expect(payment.source.gateway_payment_profile_id).to eq card_id
|
||||
end
|
||||
end
|
||||
|
||||
context "when called from Stripe SCA" do
|
||||
let(:customer_response_body) {
|
||||
JSON.generate(customer: customer_id, id: card_id, sources: { data: [{ id: "1" }] })
|
||||
}
|
||||
|
||||
it "fetches the customer id and the card id from the correct response fields" do
|
||||
profile_storer.create_customer_from_token
|
||||
|
||||
expect(payment.source.gateway_customer_profile_id).to eq customer_id
|
||||
expect(payment.source.gateway_payment_profile_id).to eq card_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,16 +7,6 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:enterprise) { create(:distributor_enterprise) }
|
||||
let!(:exchange) do
|
||||
create(
|
||||
:exchange,
|
||||
order_cycle: order_cycle,
|
||||
sender: order_cycle.coordinator,
|
||||
receiver: enterprise,
|
||||
incoming: false,
|
||||
pickup_time: "Monday"
|
||||
)
|
||||
end
|
||||
let!(:shipping_method) do
|
||||
create(
|
||||
:shipping_method,
|
||||
|
||||
281
spec/requests/checkout/stripe_sca_spec.rb
Normal file
281
spec/requests/checkout/stripe_sca_spec.rb
Normal file
@@ -0,0 +1,281 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "checking out an order with a Stripe SCA payment method", type: :request do
|
||||
include ShopWorkflow
|
||||
include AuthenticationWorkflow
|
||||
include OpenFoodNetwork::ApiHelper
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:enterprise) { create(:distributor_enterprise) }
|
||||
let!(:shipping_method) do
|
||||
create(
|
||||
:shipping_method,
|
||||
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0),
|
||||
distributors: [enterprise]
|
||||
)
|
||||
end
|
||||
let!(:payment_method) { create(:stripe_sca_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(:stripe_payment_method) { "pm_123" }
|
||||
let(:new_stripe_payment_method) { "new_pm_123" }
|
||||
let(:customer_id) { "cus_A123" }
|
||||
let(:payments_attributes) do
|
||||
{
|
||||
payment_method_id: payment_method.id,
|
||||
source_attributes: {
|
||||
gateway_payment_profile_id: stripe_payment_method,
|
||||
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
|
||||
let(:payment_intent_response_mock) do
|
||||
{ status: 200, body: JSON.generate(object: "payment_intent", amount: 2000, charges: { data: [{ id: "ch_1234", amount: 2000 }]}) }
|
||||
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 }
|
||||
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
|
||||
order.update_attributes(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
|
||||
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/payment_intents")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: /#{stripe_payment_method}.*#{order.number}/)
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "and the paymeent intent request is successful" do
|
||||
it "should process the payment without storing card details" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
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 stripe_payment_method
|
||||
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 payment intent request returns an error message" do
|
||||
let(:payment_intent_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "payment-intent-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
|
||||
let(:payment_method_response_mock) do
|
||||
{
|
||||
status: 200,
|
||||
body: JSON.generate(id: new_stripe_payment_method, customer: customer_id)
|
||||
}
|
||||
end
|
||||
|
||||
let(:customer_response_mock) do
|
||||
{
|
||||
status: 200,
|
||||
body: JSON.generate(id: customer_id, sources: { data: [{ id: "1" }] })
|
||||
}
|
||||
end
|
||||
|
||||
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: { email: order.email })
|
||||
.to_return(customer_response_mock)
|
||||
|
||||
# Requests a payment method from the newly saved card
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{stripe_payment_method}/attach")
|
||||
.with(body: { customer: customer_id })
|
||||
.to_return(payment_method_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
|
||||
it "should process the payment, and stores the card/customer details" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
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 new_stripe_payment_method
|
||||
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 customer request returns an error message" do
|
||||
let(:customer_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "customer-store-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
|
||||
expect(json_response["flash"]["error"])
|
||||
.to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'customer-store-failure'))
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the payment intent request returns an error message" do
|
||||
let(:payment_intent_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "payment-intent-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the payment_method request returns an error message" do
|
||||
let(:payment_method_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "payment-method-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
|
||||
expect(json_response["flash"]["error"]).to include "payment-method-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: stripe_payment_method,
|
||||
gateway_customer_profile_id: customer_id,
|
||||
last_digits: "4321",
|
||||
cc_type: "master",
|
||||
first_name: "Sammy",
|
||||
last_name: "Signpost",
|
||||
month: 11, year: 2026
|
||||
)
|
||||
end
|
||||
|
||||
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}.*#{stripe_payment_method}})
|
||||
.to_return(payment_intent_response_mock)
|
||||
end
|
||||
|
||||
context "and the payment intent and payment method requests are accepted" do
|
||||
it "should process the payment, and keep the profile ids and other card details" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
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 stripe_payment_method
|
||||
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 payment intent request returns an error message" do
|
||||
let(:payment_intent_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "payment-intent-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user