From 5724c3bb0af61dfc206c91752ab6380ec19344c2 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:23:56 +0000 Subject: [PATCH 01/21] Add code from ActiveMerchant v1.98.0 that supports the Stripe Payment Intents API This commit can be reverted once we upgrade to v1.98.0 --- .../billing/gateways/stripe_decorator.rb | 16 ++ .../gateways/stripe_payment_intents.rb | 242 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 lib/active_merchant/billing/gateways/stripe_decorator.rb create mode 100644 lib/active_merchant/billing/gateways/stripe_payment_intents.rb diff --git a/lib/active_merchant/billing/gateways/stripe_decorator.rb b/lib/active_merchant/billing/gateways/stripe_decorator.rb new file mode 100644 index 0000000000..5d3aa90958 --- /dev/null +++ b/lib/active_merchant/billing/gateways/stripe_decorator.rb @@ -0,0 +1,16 @@ +# 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(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/)) + [response['customer'], response['id']].join('|') + else + response['id'] + end + end +end diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb new file mode 100644 index 0000000000..f022c68323 --- /dev/null +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -0,0 +1,242 @@ +# 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] + CONFIRM_INTENT_ATTRIBUTES = %i[receipt_email return_url save_payment_method setup_future_usage off_session] + UPDATE_INTENT_ATTRIBUTES = %i[description statement_descriptor receipt_email setup_future_usage] + 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 = {} + post[:cancellation_reason] = options[:cancellation_reason] if ALLOWED_CANCELLATION_REASONS.include?(options[:cancellation_reason]) + 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'] + 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 + post[:confirmation_method] = confirmation_method if ALLOWED_METHOD_STATES.include?(confirmation_method) + 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 = {}) + post[:setup_future_usage] = options[:setup_future_usage] if %w( on_session off_session ).include?(options[:setup_future_usage]) + post[:off_session] = options[:off_session] if options[:off_session] && options[:confirm] == true + post + end + + def add_connected_account(post, options = {}) + return unless transfer_data = options[:transfer_data] + post[:transfer_data] = {} + post[:transfer_data][:destination] = transfer_data[:destination] if transfer_data[:destination] + 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] + post[:shipping][:address][:country] = shipping[:address][:country] if shipping[:address][:country] + post[:shipping][:address][:line2] = shipping[:address][:line2] if shipping[:address][:line2] + post[:shipping][:address][:postal_code] = shipping[:address][:postal_code] if shipping[:address][:postal_code] + 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 From f691d1aafd2c826ec97ebf85e0696e30c9a5d7c1 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:27:17 +0000 Subject: [PATCH 02/21] Add new payment method StripeSCA that will use the Stripe Payment Intents API instead of the Stripe Charges API that the current StripeConnect gatreway uses --- .../darkswarm/services/checkout.js.coffee | 4 +- .../services/stripe_elements.js.coffee | 22 ++++- .../spree/admin/payment_methods_controller.rb | 16 +++- app/models/spree/gateway/stripe_sca.rb | 88 +++++++++++++++++++ .../_provider_settings.html.haml | 3 + config/application.rb | 1 + 6 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 app/models/spree/gateway/stripe_sca.rb diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 204c35ac66..83cf018365 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -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 diff --git a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee index 32b0535251..89b79e78fd 100644 --- a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee +++ b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee @@ -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,24 @@ 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 diff --git a/app/controllers/spree/admin/payment_methods_controller.rb b/app/controllers/spree/admin/payment_methods_controller.rb index 53bf22d06a..ad0bff9a3b 100644 --- a/app/controllers/spree/admin/payment_methods_controller.rb +++ b/app/controllers/spree/admin/payment_methods_controller.rb @@ -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,16 @@ module Spree params[:payment_method][:preferred_enterprise_id] = @stripe_account_holder.id end + + def stripe_payment_method? + @payment_method.try(:type) == "Spree::Gateway::StripeConnect" || + @payment_method.try(:type) == "Spree::Gateway::StripeSCA" + end + + def stripe_provider?(provider) + provider.name.ends_with?("StripeConnect") || + provider.name.ends_with?("StripeSCA") + end end end end diff --git a/app/models/spree/gateway/stripe_sca.rb b/app/models/spree/gateway/stripe_sca.rb new file mode 100644 index 0000000000..c8f4d56637 --- /dev/null +++ b/app/models/spree/gateway/stripe_sca.rb @@ -0,0 +1,88 @@ +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 > 0 + + errors.add(:stripe_account_owner, I18n.t(:error_required)) + end + end + end +end diff --git a/app/views/spree/admin/payment_methods/_provider_settings.html.haml b/app/views/spree/admin/payment_methods/_provider_settings.html.haml index c64ad9f1d2..38f2ff06e9 100644 --- a/app/views/spree/admin/payment_methods/_provider_settings.html.haml +++ b/app/views/spree/admin/payment_methods/_provider_settings.html.haml @@ -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 diff --git a/config/application.rb b/config/application.rb index 057d50ff2f..2799e52d2f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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. From 283abf9a88baaaaaba0bd9e14bb36ef5b0b5e0d9 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:42:01 +0000 Subject: [PATCH 03/21] Remove dead code from Stripe connect gateway Update Source is dead since a74c502fd9b1c34b3f76d15601e06d532c2dbfc8 --- app/models/spree/gateway/stripe_connect.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/models/spree/gateway/stripe_connect.rb b/app/models/spree/gateway/stripe_connect.rb index 052423e5e0..d1a7a70c2b 100644 --- a/app/models/spree/gateway/stripe_connect.rb +++ b/app/models/spree/gateway/stripe_connect.rb @@ -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 From a52c4b542c2c7a604f8d9383f2b419ea48461794 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:45:26 +0000 Subject: [PATCH 04/21] Make destroy stored cards work for stripe SCA by setting stripe account id before making the call to the stripe api This account id cannot be sent when dealing with the old StripeConnect gateway --- app/controllers/spree/credit_cards_controller.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb index 79f60b4323..a46937eb6c 100644 --- a/app/controllers/spree/credit_cards_controller.rb +++ b/app/controllers/spree/credit_cards_controller.rb @@ -54,10 +54,17 @@ 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) + options = { stripe_account: stripe_account_id } if @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" + + 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 From 9fa4bad0b45e30da7e9e414d392961135d7c093f Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:48:01 +0000 Subject: [PATCH 05/21] Add stripe SCA checkotu payment template and move stripe object definition to it and the other stripe template We need to set the stripe object with the stripe account id to work with the payment intents api but we cannot set it to work with the stripe charges api This makes the two payment methods incompatible: a given enterprise cannot use both the old stripe integration and this new one at the same time. --- app/views/checkout/_payment.html.haml | 5 ----- .../spree/checkout/payment/_stripe.html.haml | 5 +++++ .../checkout/payment/_stripe_sca.html.haml | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 app/views/spree/checkout/payment/_stripe_sca.html.haml diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index fdbd103203..928578158f 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -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"} diff --git a/app/views/spree/checkout/payment/_stripe.html.haml b/app/views/spree/checkout/payment/_stripe.html.haml index eace9f00a7..2929b90d3e 100644 --- a/app/views/spree/checkout/payment/_stripe.html.haml +++ b/app/views/spree/checkout/payment/_stripe.html.haml @@ -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') diff --git a/app/views/spree/checkout/payment/_stripe_sca.html.haml b/app/views/spree/checkout/payment/_stripe_sca.html.haml new file mode 100644 index 0000000000..00ded42afe --- /dev/null +++ b/app/views/spree/checkout/payment/_stripe_sca.html.haml @@ -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') From db1065a69e60a06a596a44c48081d2f09e029e0c Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 17:49:03 +0000 Subject: [PATCH 06/21] Make saving a card on checkout work with the payment intents api by making profile storer work with the slightly different api responses from stripe --- lib/stripe/profile_storer.rb | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb index 1b8928e884..1d6cfd7f82 100644 --- a/lib/stripe/profile_storer.rb +++ b/lib/stripe/profile_storer.rb @@ -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 @@ -14,7 +15,11 @@ module Stripe response = @provider.store(token, options) if response.success? - attrs = source_attrs_from(response) + if response.params['customer'] # Payment Intents API + attrs = stripe_sca_attrs_from(response) + else + attrs = stripe_connect_attrs_from(response) + end @payment.source.update_attributes!(attrs) else @payment.__send__(:gateway_error, response.message) @@ -24,11 +29,13 @@ module Stripe private def options - { - email: @payment.order.email, - login: Stripe.api_key, - address: address_for(@payment) - } + options = { + email: @payment.order.email, + login: Stripe.api_key, + address: address_for(@payment) + } + options = options.merge({ stripe_account: @stripe_account_id }) if @stripe_account_id.present? + options end def address_for(payment) @@ -52,9 +59,17 @@ module Stripe end end - def source_attrs_from(response) + def stripe_sca_attrs_from(response) { - cc_type: @payment.source.cc_type, # side-effect of update_source! + cc_type: @payment.source.cc_type, + gateway_customer_profile_id: response.params['customer'], + gateway_payment_profile_id: response.params['id'] + } + end + + def stripe_connect_attrs_from(response) + { + cc_type: @payment.source.cc_type, gateway_customer_profile_id: response.params['id'], gateway_payment_profile_id: response.params['default_source'] || response.params['default_card'] } From c773cde191924dc54bc23ae931f6021b652ba230 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 18:03:07 +0000 Subject: [PATCH 07/21] Add admin payment template for stripe sca and respective js code to make it work --- .../admin/payments/services/payment.js.coffee | 11 +++++++++++ .../payments/services/stripe_elements.js.coffee | 17 ++++++++++++++++- .../services/stripe_elements.js.coffee | 3 +-- .../payments/source_forms/_stripe_sca.html.haml | 16 ++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml diff --git a/app/assets/javascripts/admin/payments/services/payment.js.coffee b/app/assets/javascripts/admin/payments/services/payment.js.coffee index f079137818..f5cab3061c 100644 --- a/app/assets/javascripts/admin/payments/services/payment.js.coffee +++ b/app/assets/javascripts/admin/payments/services/payment.js.coffee @@ -30,11 +30,22 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren month: @form_data.card.exp_month year: @form_data.card.exp_year } + when 'stripe_sca' + angular.extend munged_payment.payment, { + source_attributes: + gateway_payment_profile_id: @form_data.token + cc_type: @form_data.cc_type + last_digits: @form_data.card.last4 + month: @form_data.card.exp_month + year: @form_data.card.exp_year + } munged_payment purchase: -> if @paymentMethodType() == 'stripe' AdminStripeElements.requestToken(@form_data, @submit) + else if @paymentMethodType() == 'stripe_sca' + AdminStripeElements.createPaymentMethod(@form_data, @submit) else @submit() diff --git a/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee b/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee index 03971be228..9ecf2b1db1 100644 --- a/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee +++ b/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee @@ -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 diff --git a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee index 89b79e78fd..acd220f092 100644 --- a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee +++ b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee @@ -28,8 +28,7 @@ Darkswarm.factory 'StripeElements', ($rootScope, Loading, RailsFlashLoader) -> Loading.message = loading_message cardData = @makeCardData(secrets) - @stripe.createPaymentMethod({ type: 'card', card: @card } - @card, cardData).then (response) => + @stripe.createPaymentMethod({ type: 'card', card: @card }, @card, cardData).then (response) => if(response.error) Loading.clear() RailsFlashLoader.loadFlash({error: t("error") + ": #{response.error.message}"}) diff --git a/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml b/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml new file mode 100644 index 0000000000..b8939ea4a7 --- /dev/null +++ b/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml @@ -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 From ec7b91bb686cdc5271d9a5188ecb44d00611fcde Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 19:21:39 +0000 Subject: [PATCH 08/21] Make ProfileStorer a bit easier to read --- lib/stripe/profile_storer.rb | 42 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb index 1d6cfd7f82..50a0ba4b31 100644 --- a/lib/stripe/profile_storer.rb +++ b/lib/stripe/profile_storer.rb @@ -15,11 +15,7 @@ module Stripe response = @provider.store(token, options) if response.success? - if response.params['customer'] # Payment Intents API - attrs = stripe_sca_attrs_from(response) - else - attrs = stripe_connect_attrs_from(response) - end + attrs = source_attrs_from(response) @payment.source.update_attributes!(attrs) else @payment.__send__(:gateway_error, response.message) @@ -30,11 +26,11 @@ module Stripe def options options = { - email: @payment.order.email, - login: Stripe.api_key, - address: address_for(@payment) - } - options = options.merge({ stripe_account: @stripe_account_id }) if @stripe_account_id.present? + email: @payment.order.email, + login: Stripe.api_key, + address: address_for(@payment) + } + options = options.merge(stripe_account: @stripe_account_id) if @stripe_account_id.present? options end @@ -59,20 +55,28 @@ module Stripe end end - def stripe_sca_attrs_from(response) + def source_attrs_from(response) { cc_type: @payment.source.cc_type, - gateway_customer_profile_id: response.params['customer'], - gateway_payment_profile_id: response.params['id'] + gateway_customer_profile_id: customer_profile_id(response), + gateway_payment_profile_id: payment_profile_id(response) } end - def stripe_connect_attrs_from(response) - { - cc_type: @payment.source.cc_type, - gateway_customer_profile_id: response.params['id'], - gateway_payment_profile_id: response.params['default_source'] || response.params['default_card'] - } + def customer_profile_id(response) + if response.params['customer'] # Payment Intents API + response.params['customer'] + else + response.params['id'] + end + 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 From 1b820ea85c6df06dacd65bdb36f6d00214979194 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 19:27:16 +0000 Subject: [PATCH 09/21] Fix rubocop issues in credit_cards_controller --- .../spree/credit_cards_controller.rb | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb index a46937eb6c..3da82cd698 100644 --- a/app/controllers/spree/credit_cards_controller.rb +++ b/app/controllers/spree/credit_cards_controller.rb @@ -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,17 +58,22 @@ module Spree # Currently can only destroy the whole customer object def destroy_at_stripe - options = { stripe_account: stripe_account_id } if @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" + if @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 = 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 + 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 From ac8f3c811f182d12b59b4fd5b90e011ee68d9207 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Mon, 13 Jan 2020 19:57:45 +0000 Subject: [PATCH 10/21] Fix rubocop issues in some stripe integration related files --- .rubocop_manual_todo.yml | 6 ++ app/models/spree/gateway/stripe_sca.rb | 4 +- .../billing/gateways/stripe_decorator.rb | 5 +- .../gateways/stripe_payment_intents.rb | 92 +++++++++++++------ lib/stripe/profile_storer.rb | 6 +- 5 files changed, 76 insertions(+), 37 deletions(-) diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index cec6bd232e..bc88b63cd3 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -413,6 +413,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 @@ -506,6 +508,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 @@ -531,6 +534,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 @@ -600,6 +604,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 @@ -663,6 +668,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 diff --git a/app/models/spree/gateway/stripe_sca.rb b/app/models/spree/gateway/stripe_sca.rb index c8f4d56637..931b178475 100644 --- a/app/models/spree/gateway/stripe_sca.rb +++ b/app/models/spree/gateway/stripe_sca.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'stripe/profile_storer' require 'active_merchant/billing/gateways/stripe_payment_intents' require 'active_merchant/billing/gateways/stripe_decorator' @@ -79,7 +81,7 @@ module Spree end def ensure_enterprise_selected - return if preferred_enterprise_id.andand > 0 + return if preferred_enterprise_id.andand.positive? errors.add(:stripe_account_owner, I18n.t(:error_required)) end diff --git a/lib/active_merchant/billing/gateways/stripe_decorator.rb b/lib/active_merchant/billing/gateways/stripe_decorator.rb index 5d3aa90958..fcd23a6446 100644 --- a/lib/active_merchant/billing/gateways/stripe_decorator.rb +++ b/lib/active_merchant/billing/gateways/stripe_decorator.rb @@ -1,3 +1,5 @@ +# 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 @@ -7,7 +9,8 @@ ActiveMerchant::Billing::StripeGateway.class_eval do if url == 'customers' [response['id'], response.dig('sources', 'data').first&.dig('id')].join('|') - elsif method == :post && (url.match(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/)) + elsif method == :post && + (url.match(%r{customers/.*/cards}) || url.match(%r{payment_methods/.*/attach})) [response['customer'], response['id']].join('|') else response['id'] diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb index f022c68323..3326d700c1 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -1,3 +1,5 @@ +# 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 @@ -5,14 +7,18 @@ 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]. + # 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] - CONFIRM_INTENT_ATTRIBUTES = %i[receipt_email return_url save_payment_method setup_future_usage off_session] - UPDATE_INTENT_ATTRIBUTES = %i[description statement_descriptor receipt_email setup_future_usage] + 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 = {}) @@ -80,11 +86,15 @@ module ActiveMerchant #:nodoc: end def authorize(money, payment_method, options = {}) - create_intent(money, payment_method, options.merge!(confirm: true, capture_method: 'manual')) + 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')) + create_intent(money, + payment_method, + options.merge!(confirm: true, capture_method: 'automatic')) end def capture(money, intent_id, options = {}) @@ -96,7 +106,9 @@ module ActiveMerchant #:nodoc: def void(intent_id, options = {}) post = {} - post[:cancellation_reason] = options[:cancellation_reason] if ALLOWED_CANCELLATION_REASONS.include?(options[:cancellation_reason]) + 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 @@ -106,8 +118,10 @@ module ActiveMerchant #:nodoc: 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 + # 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 = {} @@ -115,21 +129,24 @@ module ActiveMerchant #:nodoc: # 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'] - end - commit(:post, "payment_methods/#{params[:payment_method]}/attach", { customer: customer_id }, options) - #else - # super(payment, options) - #end + # 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'] + 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 = {}) @@ -156,7 +173,9 @@ module ActiveMerchant #:nodoc: def add_confirmation_method(post, options) confirmation_method = options[:confirmation_method].to_s - post[:confirmation_method] = confirmation_method if ALLOWED_METHOD_STATES.include?(confirmation_method) + if ALLOWED_METHOD_STATES.include?(confirmation_method) + post[:confirmation_method] = confirmation_method + end post end @@ -168,6 +187,7 @@ module ActiveMerchant #:nodoc: 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 @@ -204,15 +224,22 @@ module ActiveMerchant #:nodoc: end def setup_future_usage(post, options = {}) - post[:setup_future_usage] = options[:setup_future_usage] if %w( on_session off_session ).include?(options[:setup_future_usage]) - post[:off_session] = options[:off_session] if options[:off_session] && options[:confirm] == true + 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] = {} - post[:transfer_data][:destination] = transfer_data[:destination] if transfer_data[:destination] + 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] @@ -222,13 +249,18 @@ module ActiveMerchant #:nodoc: 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] - post[:shipping][:address][:country] = shipping[:address][:country] if shipping[:address][:country] + if shipping[:address][:country] + post[:shipping][:address][:country] = shipping[:address][:country] + end post[:shipping][:address][:line2] = shipping[:address][:line2] if shipping[:address][:line2] - post[:shipping][:address][:postal_code] = shipping[:address][:postal_code] if shipping[:address][:postal_code] + 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] diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb index 50a0ba4b31..b4cccdb470 100644 --- a/lib/stripe/profile_storer.rb +++ b/lib/stripe/profile_storer.rb @@ -64,11 +64,7 @@ module Stripe end def customer_profile_id(response) - if response.params['customer'] # Payment Intents API - response.params['customer'] - else - response.params['id'] - end + response.params['customer'] || response.params['id'] end def payment_profile_id(response) From c7b01c37af12d9d552b9feea8e622a61e0d6152d Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 14 Jan 2020 15:59:57 +0000 Subject: [PATCH 11/21] Fix a problem in credit cards controller spec and test case where stripe_account_id must be included in the stripe api call --- app/controllers/spree/credit_cards_controller.rb | 2 +- .../spree/credit_cards_controller_spec.rb | 16 ++++++++++++++++ spec/factories.rb | 9 ++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb index 3da82cd698..e0b23277da 100644 --- a/app/controllers/spree/credit_cards_controller.rb +++ b/app/controllers/spree/credit_cards_controller.rb @@ -58,7 +58,7 @@ module Spree # Currently can only destroy the whole customer object def destroy_at_stripe - if @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" + if @credit_card.payment_method && @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" options = { stripe_account: stripe_account_id } end diff --git a/spec/controllers/spree/credit_cards_controller_spec.rb b/spec/controllers/spree/credit_cards_controller_spec.rb index e8996433f3..b9ed2ed77b 100644 --- a/spec/controllers/spree/credit_cards_controller_spec.rb +++ b/spec/controllers/spree/credit_cards_controller_spec.rb @@ -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 diff --git a/spec/factories.rb b/spec/factories.rb index 4f9c6478f5..99b704de80 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -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 From 0e815439b35a6aeb46c7564c1796a9a648100b3a Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Tue, 14 Jan 2020 18:39:09 +0000 Subject: [PATCH 12/21] Duplicate stripe_connect_spec and adapt to new stripe_sca stripe_connect_spec will be deleted at some point when all users are migrated to the sca api --- spec/requests/checkout/stripe_sca_spec.rb | 318 ++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 spec/requests/checkout/stripe_sca_spec.rb diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb new file mode 100644 index 0000000000..f86568b5b4 --- /dev/null +++ b/spec/requests/checkout/stripe_sca_spec.rb @@ -0,0 +1,318 @@ +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!(: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, + 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(: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 } + + 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 + 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/payment_intents") + .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 + + 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 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 + + 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/payment_intents") + .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 + + 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 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 + + 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 + + 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 + + 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 + quick_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/payment_intents") + .with(basic_auth: ["sk_test_12345", ""], body: %r{.*#{customer_id}.*#{order.number}.*#{card_id}.*}) + .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 + + 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 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 + + 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 + + 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 From 6bb04f6cc6992ad40169ed22d4b19ca9909f4928 Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Wed, 15 Jan 2020 12:43:27 +0000 Subject: [PATCH 13/21] Adapt stripe_sca_spec to actual stripe SCA API --- .../gateways/stripe_payment_intents.rb | 3 + spec/requests/checkout/stripe_sca_spec.rb | 145 +++++++----------- 2 files changed, 62 insertions(+), 86 deletions(-) diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb index 3326d700c1..9c5c602c7a 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -140,6 +140,9 @@ module ActiveMerchant #:nodoc: 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", diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb index f86568b5b4..33bf6fba27 100644 --- a/spec/requests/checkout/stripe_sca_spec.rb +++ b/spec/requests/checkout/stripe_sca_spec.rb @@ -29,15 +29,14 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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(: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: token, + gateway_payment_profile_id: stripe_payment_method, cc_type: "visa", last_digits: "4242", month: 10, @@ -70,6 +69,9 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques } } 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) @@ -83,32 +85,15 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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/payment_intents") - .with(basic_auth: ["sk_test_12345", ""], body: /#{token}.*#{order.number}/) - .to_return(charge_response_mock) + .with(basic_auth: ["sk_test_12345", ""], body: /#{stripe_payment_method}.*#{order.number}/) + .to_return(payment_intent_response_mock) end - context "and the charge request is successful" do + context "and the paymeent intent request is successful" do it "should process the payment without storing card details" do put update_checkout_path, params @@ -118,7 +103,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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.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" @@ -126,9 +111,9 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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" }) } + 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 @@ -136,36 +121,50 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques expect(response.status).to be 400 - expect(json_response["flash"]["error"]).to eq "charge-failure" + 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: { card: token, email: order.email }) - .to_return(store_response_mock) + .with(basic_auth: ["sk_test_12345", ""], body: { email: order.email }) + .to_return(customer_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) + # 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: /#{token}.*#{order.number}/ - ).to_return(charge_response_mock) + body: /.*#{order.number}/ + ).to_return(payment_intent_response_mock) end - context "and the store, token and charge requests are successful" do + 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 @@ -175,7 +174,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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.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" @@ -183,9 +182,9 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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" }) } + 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 @@ -194,14 +193,14 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques expect(response.status).to be 400 expect(json_response["flash"]["error"]) - .to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure')) + .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 charge request returns an error message" do - let(:charge_response_mock) do - { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } + 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 @@ -209,23 +208,22 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques expect(response.status).to be 400 - expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(json_response["flash"]["error"]).to eq "payment-intent-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" }) } + context "when the payment_metho request returns an error message" do + let(:payment_method_response_mock) do + { status: 402, body: JSON.generate(error: { message: "payment-method-failure" }) } end - # Note, no requests have been stubbed 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 "token-failure" + expect(json_response["flash"]["error"]).to include "payment-method-failure" expect(order.payments.completed.count).to be 0 end end @@ -237,7 +235,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques create( :credit_card, user_id: order.user_id, - gateway_payment_profile_id: card_id, + gateway_payment_profile_id: stripe_payment_method, gateway_customer_profile_id: customer_id, last_digits: "4321", cc_type: "master", @@ -247,27 +245,17 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques ) 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 quick_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/payment_intents") - .with(basic_auth: ["sk_test_12345", ""], body: %r{.*#{customer_id}.*#{order.number}.*#{card_id}.*}) - .to_return(charge_response_mock) + .with(basic_auth: ["sk_test_12345", ""], body: %r{#{customer_id}.*#{stripe_payment_method}}) + .to_return(payment_intent_response_mock) end - context "and the charge and token requests are accepted" do + 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 @@ -277,7 +265,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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.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" @@ -285,9 +273,9 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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" }) } + 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 @@ -295,22 +283,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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 - - expect(response.status).to be 400 - - expect(json_response["flash"]["error"]).to eq "token-error" + expect(json_response["flash"]["error"]).to eq "payment-intent-failure" expect(order.payments.completed.count).to be 0 end end From aff934c814f38f38e6badc91fb504ea678d8a52b Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Wed, 15 Jan 2020 12:51:38 +0000 Subject: [PATCH 14/21] Remove unnecessary test setup code --- spec/requests/checkout/stripe_connect_spec.rb | 10 ---------- spec/requests/checkout/stripe_sca_spec.rb | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/spec/requests/checkout/stripe_connect_spec.rb b/spec/requests/checkout/stripe_connect_spec.rb index a64bb44ab8..697664b806 100644 --- a/spec/requests/checkout/stripe_connect_spec.rb +++ b/spec/requests/checkout/stripe_connect_spec.rb @@ -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, diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb index 33bf6fba27..c5233910cc 100644 --- a/spec/requests/checkout/stripe_sca_spec.rb +++ b/spec/requests/checkout/stripe_sca_spec.rb @@ -7,16 +7,6 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques 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, From 668fd1c7c0452966b0f2e590286fbe26694aaec4 Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Wed, 15 Jan 2020 13:15:56 +0000 Subject: [PATCH 15/21] Add spec for profile storer to cover happy path for both response attribute cases: existin stripe integration and new stripe sca --- spec/lib/stripe/profile_storer_spec.rb | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 spec/lib/stripe/profile_storer_spec.rb diff --git a/spec/lib/stripe/profile_storer_spec.rb b/spec/lib/stripe/profile_storer_spec.rb new file mode 100644 index 0000000000..beec53feec --- /dev/null +++ b/spec/lib/stripe/profile_storer_spec.rb @@ -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 From b8457ebeced22747eaadfd7553eb0d7b67371d98 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 16 Jan 2020 13:18:59 +0000 Subject: [PATCH 16/21] Make profile storer a bit easier to read --- lib/stripe/profile_storer.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb index b4cccdb470..1806a48bd6 100644 --- a/lib/stripe/profile_storer.rb +++ b/lib/stripe/profile_storer.rb @@ -25,13 +25,17 @@ module Stripe private def options - options = { + { email: @payment.order.email, login: Stripe.api_key, address: address_for(@payment) - } - options = options.merge(stripe_account: @stripe_account_id) if @stripe_account_id.present? - options + }.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) From 66440f9e4c837e75e616c50f794e09589611a951 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 16 Jan 2020 13:19:17 +0000 Subject: [PATCH 17/21] Add missing translations for new payment method stripe sca --- config/locales/en.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 2393ee26d7..25af1defa2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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' From 4e84310d6344479a2841de70c21a2ce183851824 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 16 Jan 2020 18:38:29 +0000 Subject: [PATCH 18/21] Add StripeSCA where StripeConnect is treated as an exception in the setting up of process of a payment method and subscriptions Here we are copy pasting and adding stripe SCA because we are planning to delete the StripeConnect that will be replaced by the stripe sca implementation --- .../subscriptions/controllers/details_controller.js.coffee | 2 +- app/models/spree/payment_method_decorator.rb | 2 ++ app/models/subscription.rb | 2 +- app/serializers/api/admin/payment_method_serializer.rb | 2 +- app/services/subscription_validator.rb | 2 +- lib/open_food_network/subscription_payment_updater.rb | 3 ++- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee b/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee index 55d2a46b42..cb331a62f3 100644 --- a/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee +++ b/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee @@ -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 = -> diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index f32944fdcb..a6d4b19c48 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -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 diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 443a6ef7a4..03b2c0c5a7 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,5 +1,5 @@ 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 diff --git a/app/serializers/api/admin/payment_method_serializer.rb b/app/serializers/api/admin/payment_method_serializer.rb index 3d66ddbc03..18fccbe9e7 100644 --- a/app/serializers/api/admin/payment_method_serializer.rb +++ b/app/serializers/api/admin/payment_method_serializer.rb @@ -4,7 +4,7 @@ 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) diff --git a/app/services/subscription_validator.rb b/app/services/subscription_validator.rb index 8d9a678f9a..9ba52145e4 100644 --- a/app/services/subscription_validator.rb +++ b/app/services/subscription_validator.rb @@ -82,7 +82,7 @@ class SubscriptionValidator def credit_card_ok? return unless customer && payment_method - return unless payment_method.type == "Spree::Gateway::StripeConnect" + return unless payment_method.type == "Spree::Gateway::StripeConnect" || payment_method.type == "Spree::Gateway::StripeSCA" return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges return if customer.user.andand.default_card.present? diff --git a/lib/open_food_network/subscription_payment_updater.rb b/lib/open_food_network/subscription_payment_updater.rb index 1c4d275db1..d1cbcb4c51 100644 --- a/lib/open_food_network/subscription_payment_updater.rb +++ b/lib/open_food_network/subscription_payment_updater.rb @@ -30,7 +30,8 @@ module OpenFoodNetwork end def card_required? - payment.payment_method.is_a? Spree::Gateway::StripeConnect + payment.payment_method.is_a?(Spree::Gateway::StripeConnect) || + payment.payment_method.is_a?(Spree::Gateway::StripeSCA) end def card_set? From 38fd028a9f4e86ff7ed8d23e0f5f530e84d516f3 Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Thu, 16 Jan 2020 18:47:13 +0000 Subject: [PATCH 19/21] Fix some rubocop issues from previous commit --- app/controllers/spree/credit_cards_controller.rb | 3 ++- app/models/subscription.rb | 4 +++- app/serializers/api/admin/payment_method_serializer.rb | 3 ++- app/services/subscription_validator.rb | 7 ++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb index e0b23277da..fbad74e943 100644 --- a/app/controllers/spree/credit_cards_controller.rb +++ b/app/controllers/spree/credit_cards_controller.rb @@ -58,7 +58,8 @@ module Spree # Currently can only destroy the whole customer object def destroy_at_stripe - if @credit_card.payment_method && @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" + if @credit_card.payment_method && + @credit_card.payment_method.type == "Spree::Gateway::StripeSCA" options = { stripe_account: stripe_account_id } end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 03b2c0c5a7..ca1fd676e8 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,5 +1,7 @@ class Subscription < ActiveRecord::Base - ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check", "Spree::Gateway::StripeConnect", "Spree::Gateway::StripeSCA"].freeze + ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check", + "Spree::Gateway::StripeConnect", + "Spree::Gateway::StripeSCA"].freeze belongs_to :shop, class_name: 'Enterprise' belongs_to :customer diff --git a/app/serializers/api/admin/payment_method_serializer.rb b/app/serializers/api/admin/payment_method_serializer.rb index 18fccbe9e7..9862b81dcd 100644 --- a/app/serializers/api/admin/payment_method_serializer.rb +++ b/app/serializers/api/admin/payment_method_serializer.rb @@ -4,7 +4,8 @@ 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::StripeConnect' || + object.type == 'Spree::Gateway::StripeSCA' Api::Admin::PaymentMethod::StripeSerializer.new(object) else Api::Admin::PaymentMethod::BaseSerializer.new(object) diff --git a/app/services/subscription_validator.rb b/app/services/subscription_validator.rb index 9ba52145e4..4cce8a3af3 100644 --- a/app/services/subscription_validator.rb +++ b/app/services/subscription_validator.rb @@ -82,13 +82,18 @@ class SubscriptionValidator def credit_card_ok? return unless customer && payment_method - return unless payment_method.type == "Spree::Gateway::StripeConnect" || payment_method.type == "Spree::Gateway::StripeSCA" + 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? From 6fb74c88cdd42d5d49413ff070a2ec9021fc378b Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Sun, 19 Jan 2020 21:33:56 +0000 Subject: [PATCH 20/21] Fix a typo --- spec/requests/checkout/stripe_sca_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb index c5233910cc..dcdb0f4291 100644 --- a/spec/requests/checkout/stripe_sca_spec.rb +++ b/spec/requests/checkout/stripe_sca_spec.rb @@ -203,7 +203,7 @@ describe "checking out an order with a Stripe SCA payment method", type: :reques end end - context "when the payment_metho request returns an error message" do + 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 From b3ac5d8f4187454deaf2347bed35a3685cfb32cf Mon Sep 17 00:00:00 2001 From: luisramos0 Date: Sun, 19 Jan 2020 21:46:52 +0000 Subject: [PATCH 21/21] Improve code readability a little --- .../admin/payments/services/payment.js.coffee | 11 +---------- .../spree/admin/payment_methods_controller.rb | 7 +++---- lib/open_food_network/subscription_payment_updater.rb | 4 ++-- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/admin/payments/services/payment.js.coffee b/app/assets/javascripts/admin/payments/services/payment.js.coffee index f5cab3061c..a87497226d 100644 --- a/app/assets/javascripts/admin/payments/services/payment.js.coffee +++ b/app/assets/javascripts/admin/payments/services/payment.js.coffee @@ -21,16 +21,7 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren year: @form_data.card_year verification_value: @form_data.card_verification_value } - when 'stripe' - angular.extend munged_payment.payment, { - source_attributes: - gateway_payment_profile_id: @form_data.token - cc_type: @form_data.cc_type - last_digits: @form_data.card.last4 - month: @form_data.card.exp_month - year: @form_data.card.exp_year - } - when 'stripe_sca' + when 'stripe', 'stripe_sca' angular.extend munged_payment.payment, { source_attributes: gateway_payment_profile_id: @form_data.token diff --git a/app/controllers/spree/admin/payment_methods_controller.rb b/app/controllers/spree/admin/payment_methods_controller.rb index ad0bff9a3b..c13712b9a1 100644 --- a/app/controllers/spree/admin/payment_methods_controller.rb +++ b/app/controllers/spree/admin/payment_methods_controller.rb @@ -149,13 +149,12 @@ module Spree end def stripe_payment_method? - @payment_method.try(:type) == "Spree::Gateway::StripeConnect" || - @payment_method.try(:type) == "Spree::Gateway::StripeSCA" + ["Spree::Gateway::StripeConnect", + "Spree::Gateway::StripeSCA"].include? @payment_method.try(:type) end def stripe_provider?(provider) - provider.name.ends_with?("StripeConnect") || - provider.name.ends_with?("StripeSCA") + provider.name.ends_with?("StripeConnect", "StripeSCA") end end end diff --git a/lib/open_food_network/subscription_payment_updater.rb b/lib/open_food_network/subscription_payment_updater.rb index d1cbcb4c51..4a9c8fa144 100644 --- a/lib/open_food_network/subscription_payment_updater.rb +++ b/lib/open_food_network/subscription_payment_updater.rb @@ -30,8 +30,8 @@ module OpenFoodNetwork end def card_required? - payment.payment_method.is_a?(Spree::Gateway::StripeConnect) || - payment.payment_method.is_a?(Spree::Gateway::StripeSCA) + [Spree::Gateway::StripeConnect, + Spree::Gateway::StripeSCA].include? payment.payment_method.class end def card_set?