Skip to content

Commit 9c940fe

Browse files
eliakennyadslwaiting-for-dev
committed
Extract the customer related methods from payment method to a dedicated class
- Use a polymorphic association to allow connecting it to either users or orders (for guest users) - Stop using metadata since we now store the id and we can rely on the database for fetching it - No need for fetching the customer form the API anywhere in the current implementation Co-Authored-By: Alberto Vena <kennyadsl@gmail.com> Co-Authored-By: Marc Busqué <marc@lamarciana.com>
1 parent d45eeee commit 9c940fe

7 files changed

Lines changed: 104 additions & 50 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusStripe
4+
class Customer < ApplicationRecord
5+
belongs_to :payment_method
6+
7+
# Source is supposed to be a user or an order and needs to respond to #email
8+
belongs_to :source, polymorphic: true
9+
10+
def self.retrieve_or_create_stripe_customer_id(payment_method:, order:)
11+
instance = find_or_initialize_by(payment_method: payment_method, source: order.user || order)
12+
13+
instance.stripe_id ||
14+
instance.create_stripe_customer.tap { instance.update!(stripe_id: _1.id) }.id
15+
end
16+
17+
def create_stripe_customer
18+
payment_method.gateway.request { Stripe::Customer.create(email: source.email) }
19+
end
20+
end
21+
end

app/models/solidus_stripe/payment_intent.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ def stripe_intent
2121
end
2222

2323
def create_stripe_intent(stripe_intent_options)
24-
customer = payment_method.customer_for(order)
24+
stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id(
25+
payment_method: payment_method,
26+
order: order
27+
)
2528

2629
payment_method.gateway.request do
2730
Stripe::PaymentIntent.create({
@@ -35,7 +38,7 @@ def create_stripe_intent(stripe_intent_options)
3538
# avoid capturing the money before the order is completed.
3639
capture_method: 'manual',
3740
setup_future_usage: payment_method.preferred_setup_future_usage.presence,
38-
customer: customer,
41+
customer: stripe_customer_id,
3942
metadata: { solidus_order_number: order.number },
4043
}.merge(stripe_intent_options))
4144
end

app/models/solidus_stripe/payment_method.rb

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -52,52 +52,6 @@ def gateway_class
5252
end
5353
end
5454

55-
concerning :Customer do
56-
def customer_for(order)
57-
if order.user
58-
find_customer_for_user(order.user) || create_customer_for_user(order.user)
59-
else
60-
find_customer_for_order(order) || create_customer_for_order(order)
61-
end
62-
end
63-
64-
def find_customer_for_user(user)
65-
gateway.request do
66-
raise "unsupported email address: #{user.email.inspect}" if user.email.include?("'")
67-
68-
Stripe::Customer.search(
69-
query: "metadata['solidus_user_id']:'#{user.id}' AND email:'#{user.email}'"
70-
).first
71-
end
72-
end
73-
74-
def create_customer_for_user(user)
75-
gateway.request do
76-
Stripe::Customer.create(
77-
email: user.email,
78-
metadata: { solidus_user_id: user.id },
79-
)
80-
end
81-
end
82-
83-
def find_customer_for_order(order)
84-
gateway.request do
85-
Stripe::Customer.search(
86-
query: "metadata['solidus_order_number']:'#{order.number}'"
87-
).first
88-
end
89-
end
90-
91-
def create_customer_for_order(order)
92-
gateway.request do
93-
Stripe::Customer.create(
94-
email: order.email,
95-
metadata: { solidus_order_number: order.number },
96-
)
97-
end
98-
end
99-
end
100-
10155
def skip_confirm_step?
10256
preferred_stripe_intents_flow == 'payment' &&
10357
preferred_skip_confirmation_for_payment_intent

app/models/solidus_stripe/setup_intent.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ def stripe_intent
2121
end
2222

2323
def create_stripe_intent(stripe_intent_options)
24-
customer = payment_method.customer_for(order)
24+
stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id(
25+
payment_method: payment_method,
26+
order: order
27+
)
2528

2629
payment_method.gateway.request do
2730
Stripe::SetupIntent.create({
28-
customer: customer,
31+
customer: stripe_customer_id,
2932
usage: payment_method.preferred_setup_future_usage.presence,
3033
metadata: { solidus_order_number: order.number },
3134
}.merge(stripe_intent_options))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class CreateSolidusStripeCustomers < ActiveRecord::Migration[7.0]
2+
def change
3+
create_table :solidus_stripe_customers do |t|
4+
t.references :payment_method, null: false, foreign_key: { to_table: :spree_payment_methods }
5+
6+
t.string :source_type
7+
t.integer :source_id
8+
9+
t.string :stripe_id, index: true
10+
11+
t.timestamps
12+
13+
t.index [:payment_method_id, :source_type, :source_id], unique: true, name: :payment_method_and_source
14+
end
15+
end
16+
end

lib/solidus_stripe/testing_support/factories.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,14 @@
3838
association :payment_method, factory: :stripe_payment_method
3939
slug { SecureRandom.hex(16) }
4040
end
41+
42+
factory :stripe_customer, class: 'SolidusStripe::Customer' do
43+
association :payment_method, factory: :stripe_payment_method
44+
association :source, factory: :user
45+
stripe_id { "cus_#{SecureRandom.uuid.delete('-')}" }
46+
47+
trait :guest do
48+
association :source, factory: :order, email: 'guest@example.com', user: nil
49+
end
50+
end
4151
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require 'solidus_stripe_spec_helper'
4+
5+
RSpec.describe SolidusStripe::Customer, type: :model do
6+
describe ".retrieve_or_create_stripe_customer_id" do
7+
context 'with an existing customer' do
8+
it 'returns the customer_id' do
9+
user = create(:user)
10+
order = create(:order, user: user)
11+
customer = create(:stripe_customer, stripe_id: 'cus_123', source: user)
12+
13+
expect(customer.source).to be_a(Spree::User)
14+
expect(
15+
described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: customer.payment_method)
16+
).to eq('cus_123')
17+
end
18+
end
19+
20+
context 'without an existing customer' do
21+
it 'creates the customer from a user' do
22+
user = create(:user, email: 'registered@example.com')
23+
order = create(:order, user: user)
24+
payment_method = create(:stripe_payment_method)
25+
26+
stripe_customer = Stripe::Customer.construct_from(id: 'cus_123')
27+
allow(Stripe::Customer).to receive(:create).with(email: 'registered@example.com').and_return(stripe_customer)
28+
29+
expect(
30+
described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: payment_method)
31+
).to eq('cus_123')
32+
end
33+
34+
it 'creates the customer from a guest order' do
35+
payment_method = create(:stripe_payment_method)
36+
order = create(:order, user: nil, email: 'guest@example.com')
37+
38+
stripe_customer = Stripe::Customer.construct_from(id: 'cus_123')
39+
allow(Stripe::Customer).to receive(:create).with(email: 'guest@example.com').and_return(stripe_customer)
40+
41+
expect(
42+
described_class.retrieve_or_create_stripe_customer_id(order: order, payment_method: payment_method)
43+
).to eq('cus_123')
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)