Skip to content

Commit

Permalink
Sync Solidus Users and Stripe Customers
Browse files Browse the repository at this point in the history
Solves solidusio#26.
Begins to solve solidusio#74.
  • Loading branch information
brchristian committed Aug 20, 2020
1 parent 36d0f64 commit c28c1c2
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 97 deletions.
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
13 changes: 13 additions & 0 deletions app/decorators/models/spree/order_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Spree
module OrderDecorator
include StripeApiMethods

def stripe_customer_params
stripe_customer_params_from_addresses(bill_address, ship_address, email)
end

::Spree::Order.prepend(self)
end
end
40 changes: 40 additions & 0 deletions app/decorators/models/spree/user_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Spree
module UserDecorator
include StripeApiMethods

def self.prepended(base)
base.after_create :create_stripe_customer
base.after_update :update_stripe_customer
end

def stripe_customer
Stripe::Customer.retrieve(stripe_customer_id) if stripe_customer_id.present?
end

def create_stripe_customer
stripe_customer = Stripe::Customer.create(self.stripe_params)
update_column(:stripe_customer_id, stripe_customer.id)
stripe_customer
end

def update_stripe_customer
Stripe::Customer.update(stripe_customer_id, self.stripe_params)
end

def delete_stripe_customer
if stripe_customer_id.present?
deleted_user = Stripe::Customer.delete(stripe_customer_id)
update_column(:stripe_customer_id, nil)
deleted_user
end
end

def stripe_params
stripe_customer_params_from_addresses(bill_address, ship_address, email)
end

::Spree.user_class.prepend(self)
end
end
28 changes: 28 additions & 0 deletions app/models/concerns/stripe_api_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module StripeApiMethods
extend ActiveSupport::Concern

def stripe_customer_params_from_addresses(bill_address, ship_address, email)
{
address: stripe_address_hash(bill_address),
email: email,
name: bill_address.try(:name) || bill_address&.full_name,
phone: bill_address&.phone,
shipping: {
address: stripe_address_hash(ship_address),
name: ship_address.try(:name) || ship_address&.full_name,
phone: ship_address&.phone
}
}
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
}
end
end
88 changes: 23 additions & 65 deletions app/models/spree/payment_method/stripe_credit_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ class StripeCreditCard < Spree::PaymentMethod::CreditCard
preference :v3_elements, :boolean
preference :v3_intents, :boolean

CARD_TYPE_MAPPING = {
'American Express' => 'american_express',
'Diners Club' => 'diners_club',
'Visa' => 'visa'
}

delegate :create_intent, :update_intent, :confirm_intent, :show_intent, to: :gateway

def stripe_config(order)
Expand Down Expand Up @@ -103,39 +97,29 @@ 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)

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)
source = payment.source
order = payment.order
user = source.user || order.user

# Find or create Stripe customer
stripe_customer = user&.stripe_customer || Stripe::Customer.create(order.stripe_customer_params)

# Create new Stripe card / payment method and attach to
# (new or existing) Stripe customer
if source.gateway_payment_profile_id&.starts_with?('pm_')
stripe_payment_method = Stripe::PaymentMethod.attach(source.gateway_payment_profile_id, customer: stripe_customer)
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)
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 @@ -164,32 +148,6 @@ def options_for_purchase_or_auth(money, creditcard, transaction_options)
end
[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
end
end
end
end
1 change: 1 addition & 0 deletions config/initializers/solidus_stripe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stripe.api_key = Spree::PaymentMethod::StripeCreditCard.last&.preferences&.dig(:secret_key)
25 changes: 25 additions & 0 deletions db/migrate/20200726022620_add_stripe_customer_id_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

class AddStripeCustomerIdToUsers < SolidusSupport::Migration[5.1]
def up
add_column :spree_users, :stripe_customer_id, :string, unique: true

Spree::User.includes(bill_address: :country, ship_address: :country).find_each do |u|
user_stripe_payment_sources = user&.wallet&.wallet_payment_sources&.select do |wps|
wps.payment_source.payment_method.type == 'Spree::PaymentMethod::StripeCreditCard'
end
payment_customer_id = user_stripe_payment_sources&.map { |ps| ps&.payment_source&.gateway_customer_profile_id }.compact.last

if payment_customer_id.present?
u.update_column(:stripe_customer_id, payment_customer_id)
u.update_stripe_customer
else
u.create_stripe_customer
end
end
end

def down
remove_column :spree_users, :stripe_customer_id
end
end
1 change: 1 addition & 0 deletions lib/solidus_stripe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
require "solidus_support"
require "solidus_stripe/engine"
require "solidus_stripe/version"
require "stripe"
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
Loading

0 comments on commit c28c1c2

Please sign in to comment.