Skip to content

Commit

Permalink
refactor create_profile to use Stripe gem
Browse files Browse the repository at this point in the history
  • Loading branch information
brchristian committed Aug 15, 2020
1 parent 5d43fed commit 49ab060
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 108 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ group :development, :test do
gem "pry-rails"
gem "ffaker"
gem "rails-controller-testing"
gem "stripe"
end

gemspec
Expand Down
35 changes: 35 additions & 0 deletions app/decorators/models/spree/credit_card_decorator.rb
Original file line number Diff line number Diff line change
@@ -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
113 changes: 56 additions & 57 deletions app/models/spree/payment_method/stripe_credit_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,47 +103,67 @@ 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

user_stripe_payment_sources = payment.source.user&.wallet&.wallet_payment_sources&.select do |wps|
# 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
if user_stripe_payment_sources.present?
customer_id = user_stripe_payment_sources.map {|ps| ps.payment_source&.gateway_customer_profile_id }.compact.last
options[:customer] = customer_id
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: options[:customer] ? response.params['customer'] : response.params['id'],
gateway_payment_profile_id: options[:customer] ? response.params['id'] : response.params['default_source'] || response.params['default_card']
)
end
else
payment.send(:gateway_error, response.message)
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
address = user&.bill_address || payment.bill_address
ship_address = user&.ship_address || payment.ship_address
Stripe::Customer.create({
address: {
city: address&.city,
country: address&.country&.iso,
line1: address&.address1,
line2: address&.address2,
postal_code: address&.zipcode,
state: address&.state_text
}.compact,
email: user&.email || order.email,
# full_name is deprecated in favor of name as of Solidus 3.0
name: address.try(:name) || address&.full_name,
phone: address&.phone,
shipping: {
address: {
city: ship_address&.city,
country: ship_address&.country&.iso,
line1: ship_address&.address1,
line2: ship_address&.address2,
postal_code: ship_address&.zipcode,
state: ship_address&.state_text
}.compact,
# 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
}.compact.reject{ |_, value| value.blank? }
}.compact.reject{ |_, value| value.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

Expand Down Expand Up @@ -173,27 +193,6 @@ 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
end

def update_source!(source)
source.cc_type = CARD_TYPE_MAPPING[source.cc_type] if CARD_TYPE_MAPPING.include?(source.cc_type)
source
Expand Down
1 change: 1 addition & 0 deletions solidus_stripe.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'solidus_core', ['>= 2.3', '< 3']
spec.add_dependency 'solidus_support', '~> 0.5'
spec.add_dependency 'activemerchant', '>= 1.100'
spec.add_dependency 'stripe'

spec.add_development_dependency 'solidus_dev_support'
end
28 changes: 14 additions & 14 deletions spec/features/stripe_checkout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,63 +124,63 @@
end

it "shows an error with a missing credit card number", js: true do
fill_in_card({ number: "", code: "" })
fill_in_card(number: "", code: "")
click_button "Save and Continue"
expect(page).to have_content("Could not find payment information")
end

it "shows an error with a missing expiration date", js: true do
fill_in_card({ exp_month: "", exp_year: "" })
fill_in_card(exp_month: "", exp_year: "")
click_button "Save and Continue"
expect(page).to have_content("Your card's expiration year is invalid.")
end

it "shows an error with an invalid credit card number", js: true do
fill_in_card({ number: "1111 1111 1111 1111" })
fill_in_card(number: "1111 1111 1111 1111")
click_button "Save and Continue"
expect(page).to have_content("Your card number is incorrect.")
end

it "shows an error with invalid security fields", js: true do
fill_in_card({ code: "12" })
fill_in_card(code: "12")
click_button "Save and Continue"
expect(page).to have_content("Your card's security code is invalid.")
end

it "shows an error with invalid expiry fields", js: true do
fill_in_card({ exp_month: "00" })
fill_in_card(exp_month: "00")
click_button "Save and Continue"
expect(page).to have_content("Your card's expiration month is invalid.")
end
end

shared_examples "Stripe Elements invalid payments" do
it "shows an error with a missing credit card number" do
fill_in_card({ number: "" })
fill_in_card(number: "")
click_button "Save and Continue"
expect(page).to have_content("Your card number is incomplete.")
end

it "shows an error with a missing expiration date" do
fill_in_card({ exp_month: "", exp_year: "" })
fill_in_card(exp_month: "", exp_year: "")
click_button "Save and Continue"
expect(page).to have_content("Your card's expiration date is incomplete.")
end

it "shows an error with an invalid credit card number" do
fill_in_card({ number: "1111 1111 1111 1111" })
fill_in_card(number: "1111 1111 1111 1111")
click_button "Save and Continue"
expect(page).to have_content("Your card number is invalid.")
end

it "shows an error with invalid security fields" do
fill_in_card({ code: "12" })
fill_in_card(code: "12")
click_button "Save and Continue"
expect(page).to have_content("Your card's security code is incomplete.")
end

it "shows an error with invalid expiry fields" do
fill_in_card({ exp_month: "01", exp_year: "3" })
fill_in_card(exp_month: "01", exp_year: "3")
click_button "Save and Continue"
expect(page).to have_content("Your card's expiration date is incomplete.")
end
Expand Down Expand Up @@ -308,7 +308,7 @@

context "when using a card without enough money" do
it "fails the payment" do
fill_in_card({ number: "4000 0000 0000 9995" })
fill_in_card(number: "4000 0000 0000 9995")
click_button "Save and Continue"

expect(page).to have_content "Your card has insufficient funds."
Expand All @@ -317,7 +317,7 @@

context "when entering the wrong 3D verification code" do
it "fails the payment" do
fill_in_card({ number: "4000 0084 0000 1629" })
fill_in_card(number: "4000 0084 0000 1629")
click_button "Save and Continue"

within_3d_secure_modal do
Expand Down Expand Up @@ -414,7 +414,7 @@
let(:regular_card) { "4242 4242 4242 4242" }

it "voids the first stripe payment and successfully pays with 3DS card" do
fill_in_card({ number: regular_card })
fill_in_card(number: regular_card)
click_button "Save and Continue"

expect(page).to have_content "Ending in #{regular_card.last(4)}"
Expand Down Expand Up @@ -481,7 +481,7 @@ def within_3d_secure_modal
end

def authenticate_3d_secure_card(card_number)
fill_in_card({ number: card_number })
fill_in_card(number: card_number)
click_button "Save and Continue"

within_3d_secure_modal do
Expand Down
32 changes: 19 additions & 13 deletions spec/features/stripe_customer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@

shared_examples "Maintain Consistent Stripe Customer Across Purchases" do
it "can re-use saved cards and maintain the same Stripe payment ID and customer ID", js: true do

choose "Use an existing card on file"
click_button "Save and Continue"

Expand All @@ -110,38 +109,38 @@
click_button "Place Order"
expect(page).to have_content("Your order has been processed successfully")

user = Spree::User.find_by_email("mary@example.com")
user = Spree::User.find_by(email: "mary@example.com")
user_sources = user.wallet.wallet_payment_sources
expect(user_sources.size).to eq(1)

user_card = user_sources.first.payment_source
expect(user_card.gateway_customer_profile_id).to start_with 'cus_'
expect(user_card.gateway_payment_profile_id).to start_with 'card_'
expect(user_card.gateway_payment_profile_id).to start_with (preferred_v3_intents ? 'pm_' : 'card_')

stripe_customer = Stripe::Customer.retrieve(user_card.gateway_customer_profile_id)
expect(stripe_customer[:email]).to eq(user.email)
expect(stripe_customer[:sources][:total_count]).to eq(1)
expect(stripe_customer[:sources][:data].first[:customer]).to eq(user_card.gateway_customer_profile_id)
expect(stripe_customer[:sources][:data].first[:id]).to eq(user_card.gateway_payment_profile_id)
stripe_customer_cards = Stripe::PaymentMethod.list(customer: stripe_customer, type: 'card')
expect(stripe_customer_cards.count).to eq(1)
expect(stripe_customer_cards.first.customer).to eq(user_card.gateway_customer_profile_id)
expect(stripe_customer_cards.first.id).to eq(user_card.gateway_payment_profile_id)

expect(user.orders.map { |o| o.payments.valid.first.source.gateway_payment_profile_id }.uniq.size).to eq(1)
expect(user.orders.map { |o| o.payments.valid.first.source.gateway_customer_profile_id }.uniq.size).to eq(1)
end

it "can use a new card and maintain the same Stripe customer ID", js: true do

choose "Use a new card / payment method"
fill_in_card({ number: '5555 5555 5555 4444' })
fill_in_card(number: '5555 5555 5555 4444')
click_button "Save and Continue"

# Confirm
expect(page).to have_current_path("/checkout/confirm")

user = Spree::User.find_by_email("mary@example.com")
user = Spree::User.find_by(email: "mary@example.com")
user_cards = user.credit_cards
expect(user_cards.size).to eq(2)
expect(user_cards.pluck(:gateway_customer_profile_id)).to all( start_with 'cus_' )
expect(user_cards.pluck(:gateway_payment_profile_id)).to all( start_with 'card_' )
expect(user_cards.pluck(:gateway_payment_profile_id)).to all( start_with (preferred_v3_intents ? 'pm_' : 'card_'))
expect(user_cards.last.gateway_customer_profile_id).to eq(user_cards.first.gateway_customer_profile_id)
expect(user_cards.pluck(:gateway_customer_profile_id).uniq.size).to eq(1)

Expand All @@ -153,10 +152,10 @@
expect(user.orders.map { |o| o.payments.valid.first.source.gateway_customer_profile_id }.uniq.size).to eq(1)

stripe_customer = Stripe::Customer.retrieve(user_cards.last.gateway_customer_profile_id)
stripe_customer_cards = Stripe::PaymentMethod.list({ customer: stripe_customer.id, type: 'card' })
stripe_customer_cards = Stripe::PaymentMethod.list(customer: stripe_customer, type: 'card')
expect(stripe_customer_cards.count).to eq(2)
expect(stripe_customer_cards.data.map { |card| card.id }).to match_array(user.orders.map { |o| o.payments.valid.first.source.gateway_payment_profile_id }.uniq)
expect(stripe_customer_cards.data.map { |card| card.id }).to match_array(user_cards.pluck(:gateway_payment_profile_id))
expect(stripe_customer_cards.data.map(&:id)).to match_array(user.orders.map { |o| o.payments.valid.first.source.gateway_payment_profile_id }.uniq)
expect(stripe_customer_cards.data.map(&:id)).to match_array(user_cards.pluck(:gateway_payment_profile_id))
end
end

Expand All @@ -173,4 +172,11 @@

it_behaves_like "Maintain Consistent Stripe Customer Across Purchases"
end

context 'when using Stripe V3 API library with Intents' do
let(:preferred_v3_elements) { false }
let(:preferred_v3_intents) { true }

it_behaves_like "Maintain Consistent Stripe Customer Across Purchases"
end
end
Loading

0 comments on commit 49ab060

Please sign in to comment.