From 35b2cf1dd06271695955e8c445c38b8f82fa0c7d Mon Sep 17 00:00:00 2001 From: Brian Christian Date: Wed, 19 Aug 2020 09:46:02 -0700 Subject: [PATCH] Keep consistent customer across purchases --- .../models/spree/credit_card_decorator.rb | 35 ++++++ .../payment_method/stripe_credit_card.rb | 102 +++++++++--------- .../payment_method/stripe_credit_card_spec.rb | 50 +++++---- spec/spec_helper.rb | 4 + 4 files changed, 120 insertions(+), 71 deletions(-) create mode 100644 app/decorators/models/spree/credit_card_decorator.rb diff --git a/app/decorators/models/spree/credit_card_decorator.rb b/app/decorators/models/spree/credit_card_decorator.rb new file mode 100644 index 00000000..99090250 --- /dev/null +++ b/app/decorators/models/spree/credit_card_decorator.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Spree + module CreditCardDecorator + def cc_type=(type) + # See https://stripe.com/docs/api/cards/object#card_object-brand, + # active_merchant/lib/active_merchant/billing/credit_card.rb, + # and active_merchant/lib/active_merchant/billing/credit_card_methods.rb + # (And see also the Solidus docs at core/app/models/spree/credit_card.rb, + # which indicate that Solidus uses ActiveMerchant conventions by default.) + self[:cc_type] = case type + when 'American Express' + 'american_express' + when 'Diners Club' + 'diners_club' + when 'Discover' + 'discover' + when 'JCB' + 'jcb' + when 'MasterCard' + 'master' + when 'UnionPay' + 'unionpay' + when 'Visa' + 'visa' + when 'Unknown' + super('') + else + super(type) + end + end + + ::Spree::CreditCard.prepend(self) + end +end diff --git a/app/models/spree/payment_method/stripe_credit_card.rb b/app/models/spree/payment_method/stripe_credit_card.rb index 7e219d76..7f0ab51c 100644 --- a/app/models/spree/payment_method/stripe_credit_card.rb +++ b/app/models/spree/payment_method/stripe_credit_card.rb @@ -103,39 +103,53 @@ def cancel(response_code) def create_profile(payment) return unless payment.source.gateway_customer_profile_id.nil? - options = { - email: payment.order.email, - login: preferred_secret_key, - }.merge! address_for(payment) + order = payment.order + user = payment.source.user || order.user source = update_source!(payment.source) - if source.number.blank? && source.gateway_payment_profile_id.present? - if v3_intents? - creditcard = ActiveMerchant::Billing::StripeGateway::StripePaymentToken.new('id' => source.gateway_payment_profile_id) - else - creditcard = source.gateway_payment_profile_id - end - else - creditcard = source - end - response = gateway.store(creditcard, options) - if response.success? - if v3_intents? - payment.source.update!( - cc_type: payment.source.cc_type, - gateway_customer_profile_id: response.params['customer'], - gateway_payment_profile_id: response.params['id'] - ) - else - payment.source.update!( - 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'] - ) - end - else - payment.send(:gateway_error, response.message) + # Check to see whether a user's previous payment sources + # are linked to a Stripe account + user_stripe_payment_sources = user&.wallet&.wallet_payment_sources&.select do |wps| + wps.payment_source.payment_method.type == 'Spree::PaymentMethod::StripeCreditCard' + end + stripe_customer = if user_stripe_payment_sources.present? + customer_id = user_stripe_payment_sources.map { |ps| ps.payment_source&.gateway_customer_profile_id }.compact.last + Stripe::Customer.retrieve(customer_id) + else + bill_address = user&.bill_address || order.bill_address + ship_address = user&.ship_address || order.ship_address + Stripe::Customer.create({ + address: stripe_address_hash(bill_address), + email: user&.email || order.email, + # full_name is deprecated in favor of name as of Solidus 3.0 + name: bill_address.try(:name) || bill_address&.full_name, + phone: bill_address&.phone, + shipping: { + address: stripe_address_hash(ship_address), + # full_name is deprecated in favor of name as of Solidus 3.0 + name: ship_address.try(:name) || ship_address&.full_name, + phone: ship_address&.phone + }.reject { |_, v| v.blank? } + }.reject { |_, v| v.blank? }) + end + + # Create new Stripe card / payment method and attach to + # (new or existing) Stripe profile + if source.gateway_payment_profile_id&.starts_with?('pm_') + stripe_payment_method = Stripe::PaymentMethod.attach(source.gateway_payment_profile_id, customer: stripe_customer) + payment.source.update!( + cc_type: stripe_payment_method.card.brand, + gateway_customer_profile_id: stripe_customer.id, + gateway_payment_profile_id: stripe_payment_method.id + ) + elsif source.gateway_payment_profile_id&.starts_with?('tok_') + stripe_card = Stripe::Customer.create_source(stripe_customer.id, source: source.gateway_payment_profile_id) + payment.source.update!( + cc_type: stripe_card.brand, + gateway_customer_profile_id: stripe_customer.id, + gateway_payment_profile_id: stripe_card.id + ) end end @@ -165,25 +179,15 @@ def options_for_purchase_or_auth(money, creditcard, transaction_options) [money, creditcard, options] end - def address_for(payment) - {}.tap do |options| - if address = payment.order.bill_address - options[:address] = { - address1: address.address1, - address2: address.address2, - city: address.city, - zip: address.zipcode - } - - if country = address.country - options[:address][:country] = country.name - end - - if state = address.state - options[:address].merge!(state: state.name) - end - end - end + def stripe_address_hash(address) + { + city: address&.city, + country: address&.country&.iso, + line1: address&.address1, + line2: address&.address2, + postal_code: address&.zipcode, + state: address&.state_text + }.compact end def update_source!(source) diff --git a/spec/models/spree/payment_method/stripe_credit_card_spec.rb b/spec/models/spree/payment_method/stripe_credit_card_spec.rb index 61b8430e..7724c6f4 100644 --- a/spec/models/spree/payment_method/stripe_credit_card_spec.rb +++ b/spec/models/spree/payment_method/stripe_credit_card_spec.rb @@ -8,6 +8,7 @@ let(:source) { Spree::CreditCard.new } let(:bill_address) { nil } + let(:ship_address) { nil } let(:order) { double('Spree::Order', @@ -15,16 +16,20 @@ bill_address: bill_address, currency: 'USD', number: 'NUMBER', - total: 10.99 - ) + total: 10.99).tap do |o| + allow(o).to receive(:user) + allow(o).to receive(:bill_address).and_return(bill_address) + allow(o).to receive(:ship_address).and_return(ship_address) + end } let(:payment) { double('Spree::Payment', source: source, order: order, - amount: order.total - ) + amount: order.total).tap do |p| + allow(p).to receive(:gateway_error) + end } let(:gateway) do @@ -89,24 +94,26 @@ address2: 'Apt 303', city: 'Suzarac', zipcode: '95671', - state: double('Spree::State', name: 'Oregon'), - country: double('Spree::Country', name: 'United States')) + state_text: 'OR', + country: double('Spree::Country', name: 'United States', iso: 'US'), + full_name: 'John Smith', + phone: '555-555-5555') } it 'stores the bill address with the gateway' do - expect(subject.gateway).to receive(:store).with(payment.source, { - email: email, - login: secret_key, - + expect(Stripe::Customer).to receive(:create).with( address: { - address1: '123 Happy Road', - address2: 'Apt 303', + line1: '123 Happy Road', + line2: 'Apt 303', city: 'Suzarac', - zip: '95671', - state: 'Oregon', - country: 'United States' - } - }).and_return double.as_null_object + postal_code: '95671', + state: 'OR', + country: 'US' + }, + email: email, + name: 'John Smith', + phone: '555-555-5555' + ).and_return double.as_null_object subject.create_profile payment end @@ -114,10 +121,9 @@ context 'with an order that does not have a bill address' do it 'does not store a bill address with the gateway' do - expect(subject.gateway).to receive(:store).with(payment.source, { - email: email, - login: secret_key, - }).and_return double.as_null_object + expect(Stripe::Customer).to receive(:create).with( + email: email + ).and_return double.as_null_object subject.create_profile payment end @@ -154,7 +160,7 @@ let(:bill_address) { nil } it 'stores the profile_id as a card' do - expect(subject.gateway).to receive(:store).with(source.gateway_payment_profile_id, anything).and_return double.as_null_object + expect(Stripe::Customer).to receive(:create_source).with(anything, source: source.gateway_payment_profile_id).and_return double.as_null_object subject.create_profile payment end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 46a595ee..3630832c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,10 @@ # Requires card input helper defined in lib/solidus_stripe/testing_support/card_input_helper.rb require 'solidus_stripe/testing_support/card_input_helper' +# Stripe config +require 'stripe' +Stripe.api_key = 'sk_test_VCZnDv3GLU15TRvn8i2EsaAN' + RSpec.configure do |config| config.infer_spec_type_from_file_location! FactoryBot.find_definitions