diff --git a/app/models/solidus_stripe/customer.rb b/app/models/solidus_stripe/customer.rb new file mode 100644 index 00000000..a2d0eb1a --- /dev/null +++ b/app/models/solidus_stripe/customer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module SolidusStripe + class Customer < ApplicationRecord + belongs_to :payment_method + + # Source is supposed to be a user or an order and needs to respond to #email + belongs_to :source, polymorphic: true + + def self.retrieve_or_create_stripe_customer_id(payment_method:, order:) + instance = find_or_initialize_by(payment_method: payment_method, source: order.user || order) + + instance.stripe_id || + instance.create_stripe_customer.tap { instance.update!(stripe_id: _1.id) }.id + end + + def create_stripe_customer + payment_method.gateway.request { Stripe::Customer.create(email: source.email) } + end + end +end diff --git a/app/models/solidus_stripe/payment_intent.rb b/app/models/solidus_stripe/payment_intent.rb index f6121d2d..c83a9df5 100644 --- a/app/models/solidus_stripe/payment_intent.rb +++ b/app/models/solidus_stripe/payment_intent.rb @@ -21,7 +21,10 @@ def stripe_intent end def create_stripe_intent(stripe_intent_options) - customer = payment_method.customer_for(order) + stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id( + payment_method: payment_method, + order: order + ) payment_method.gateway.request do Stripe::PaymentIntent.create({ @@ -35,7 +38,7 @@ def create_stripe_intent(stripe_intent_options) # avoid capturing the money before the order is completed. capture_method: 'manual', setup_future_usage: payment_method.preferred_setup_future_usage.presence, - customer: customer, + customer: stripe_customer_id, metadata: { solidus_order_number: order.number }, }.merge(stripe_intent_options)) end diff --git a/app/models/solidus_stripe/payment_method.rb b/app/models/solidus_stripe/payment_method.rb index 5c15e858..1f85c05f 100644 --- a/app/models/solidus_stripe/payment_method.rb +++ b/app/models/solidus_stripe/payment_method.rb @@ -52,52 +52,6 @@ def gateway_class end end - concerning :Customer do - def customer_for(order) - if order.user - find_customer_for_user(order.user) || create_customer_for_user(order.user) - else - find_customer_for_order(order) || create_customer_for_order(order) - end - end - - def find_customer_for_user(user) - gateway.request do - raise "unsupported email address: #{user.email.inspect}" if user.email.include?("'") - - Stripe::Customer.search( - query: "metadata['solidus_user_id']:'#{user.id}' AND email:'#{user.email}'" - ).first - end - end - - def create_customer_for_user(user) - gateway.request do - Stripe::Customer.create( - email: user.email, - metadata: { solidus_user_id: user.id }, - ) - end - end - - def find_customer_for_order(order) - gateway.request do - Stripe::Customer.search( - query: "metadata['solidus_order_number']:'#{order.number}'" - ).first - end - end - - def create_customer_for_order(order) - gateway.request do - Stripe::Customer.create( - email: order.email, - metadata: { solidus_order_number: order.number }, - ) - end - end - end - def skip_confirm_step? preferred_stripe_intents_flow == 'payment' && preferred_skip_confirmation_for_payment_intent diff --git a/app/models/solidus_stripe/setup_intent.rb b/app/models/solidus_stripe/setup_intent.rb index 037dfc3b..31c52417 100644 --- a/app/models/solidus_stripe/setup_intent.rb +++ b/app/models/solidus_stripe/setup_intent.rb @@ -21,11 +21,14 @@ def stripe_intent end def create_stripe_intent(stripe_intent_options) - customer = payment_method.customer_for(order) + stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id( + payment_method: payment_method, + order: order + ) payment_method.gateway.request do Stripe::SetupIntent.create({ - customer: customer, + customer: stripe_customer_id, usage: payment_method.preferred_setup_future_usage.presence, metadata: { solidus_order_number: order.number }, }.merge(stripe_intent_options)) diff --git a/db/migrate/20230313150008_create_solidus_stripe_customers.rb b/db/migrate/20230313150008_create_solidus_stripe_customers.rb new file mode 100644 index 00000000..78b2a3cf --- /dev/null +++ b/db/migrate/20230313150008_create_solidus_stripe_customers.rb @@ -0,0 +1,16 @@ +class CreateSolidusStripeCustomers < ActiveRecord::Migration[7.0] + def change + create_table :solidus_stripe_customers do |t| + t.references :payment_method, null: false, foreign_key: { to_table: :spree_payment_methods } + + t.string :source_type + t.integer :source_id + + t.string :stripe_id, index: true + + t.timestamps + + t.index [:payment_method_id, :source_type, :source_id], unique: true, name: :payment_method_and_source + end + end +end diff --git a/lib/solidus_stripe/testing_support/factories.rb b/lib/solidus_stripe/testing_support/factories.rb index 8eab9e70..bab46062 100644 --- a/lib/solidus_stripe/testing_support/factories.rb +++ b/lib/solidus_stripe/testing_support/factories.rb @@ -38,4 +38,14 @@ association :payment_method, factory: :stripe_payment_method slug { SecureRandom.hex(16) } end + + factory :stripe_customer, class: 'SolidusStripe::Customer' do + association :payment_method, factory: :stripe_payment_method + association :source, factory: :user + stripe_id { "cus_#{SecureRandom.uuid.delete('-')}" } + + trait :guest do + association :source, factory: :order, email: 'guest@example.com', user: nil + end + end end diff --git a/spec/models/solidus_stripe/customer_spec.rb b/spec/models/solidus_stripe/customer_spec.rb new file mode 100644 index 00000000..6b4f072a --- /dev/null +++ b/spec/models/solidus_stripe/customer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'solidus_stripe_spec_helper' + +RSpec.describe SolidusStripe::Customer, type: :model do + describe ".retrieve_or_create_stripe_customer_id" do + context 'with an existing customer' do + it 'returns the customer_id' do + user = create(:user) + order = create(:order, user: user) + customer = create(:stripe_customer, stripe_id: 'cus_123', source: user) + + expect(customer.source).to be_a(Spree::User) + expect( + described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: customer.payment_method) + ).to eq('cus_123') + end + end + + context 'without an existing customer' do + it 'creates the customer from a user' do + user = create(:user, email: 'registered@example.com') + order = create(:order, user: user) + payment_method = create(:stripe_payment_method) + + stripe_customer = Stripe::Customer.construct_from(id: 'cus_123') + allow(Stripe::Customer).to receive(:create).with(email: 'registered@example.com').and_return(stripe_customer) + + expect( + described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: payment_method) + ).to eq('cus_123') + end + + it 'creates the customer from a guest order' do + payment_method = create(:stripe_payment_method) + order = create(:order, user: nil, email: 'guest@example.com') + + stripe_customer = Stripe::Customer.construct_from(id: 'cus_123') + allow(Stripe::Customer).to receive(:create).with(email: 'guest@example.com').and_return(stripe_customer) + + expect( + described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: payment_method) + ).to eq('cus_123') + end + end + end +end