Skip to content

Commit

Permalink
feat: Create customer on stripe (#268)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet authored Jun 21, 2022
1 parent de4dc92 commit e6cabb8
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 13 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ gem 'sidekiq'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem 'with_advisory_lock'

# Payment processing
gem 'stripe'

# Logging
gem 'lograge'
gem 'lograge-sql'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stripe (6.3.0)
strscan (3.0.3)
thor (1.2.1)
tilt (2.0.10)
Expand Down Expand Up @@ -338,6 +339,7 @@ DEPENDENCIES
sentry-ruby
sidekiq
simplecov
stripe
timecop
tzinfo-data
uglifier
Expand Down
16 changes: 16 additions & 0 deletions app/jobs/payment_provider_customers/stripe_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module PaymentProviderCustomers
class StripeCreateJob < ApplicationJob
queue_as :providers

retry_on Stripe::APIConnectionError, wait: :exponentially_longer, attempts: 6
retry_on Stripe::APIError, wait: :exponentially_longer, attempts: 6
retry_on Stripe::RateLimitError, wait: :exponentially_longer, attempts: 6

def perform(stripe_customer)
result = PaymentProviderCustomers::StripeService.new(stripe_customer).create
result.throw_error
end
end
end
33 changes: 29 additions & 4 deletions app/services/customers_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def create_from_api(organization:, params:)

customer.save!

assign_billing_configuration(customer, params)
# NOTE: handle configuration for configured payment providers
handle_api_billing_configuration(customer, params)

result.customer = customer
result
Expand Down Expand Up @@ -49,6 +50,9 @@ def create(**args)
vat_rate: args[:vat_rate],
)

# NOTE: handle configuration for configured payment providers
create_billing_configuration(customer)

result.customer = customer
result
rescue ActiveRecord::RecordInvalid => e
Expand All @@ -73,6 +77,7 @@ def update(**args)
customer.legal_name = args[:legal_name] if args.key?(:legal_name)
customer.legal_number = args[:legal_number] if args.key?(:legal_number)
customer.vat_rate = args[:vat_rate] if args.key?(:vat_rate)
customer.payment_provider = args[:payment_provider] if args.key?(:payment_provider)

# NOTE: Customer_id is not editable if customer is attached to subscriptions
if !customer.attached_to_subscriptions? && args.key?(:customer_id)
Expand All @@ -81,6 +86,9 @@ def update(**args)

customer.save!

# NOTE: if payment provider is updated, we need to create/update the provider customer
create_or_update_provider_customer(customer) if customer.payment_provider_previously_changed?

result.customer = customer
result
rescue ActiveRecord::RecordInvalid => e
Expand All @@ -106,8 +114,20 @@ def destroy(id:)

private

def assign_billing_configuration(customer, params)
return unless params.key?(:billing_configuration)
# NOTE: Check if a payment provider is configured in the organization and
# force creation of provider customers
def create_billing_configuration(customer)
return unless customer.organization.stripe_payment_provider&.create_customers

customer.update!(payment_provider: 'stripe')
create_or_update_provider_customer(customer)
end

def handle_api_billing_configuration(customer, params)
unless params.key?(:billing_configuration)
create_billing_configuration(customer) if customer.id_previously_changed?
return
end

billing_configuration = params[:billing_configuration]

Expand All @@ -117,8 +137,13 @@ def assign_billing_configuration(customer, params)
end

customer.update!(payment_provider: 'stripe')
create_or_update_provider_customer(customer, billing_configuration)
end

def create_or_update_provider_customer(customer, billing_configuration = {})
return unless customer.payment_provider == 'stripe'

create_result = PaymentProviderCustomers::CreateService.new(customer).create(
create_result = PaymentProviderCustomers::CreateService.new(customer).create_or_update(
customer_class: PaymentProviderCustomers::StripeCustomer,
payment_provider_id: customer.organization.stripe_payment_provider&.id,
params: billing_configuration,
Expand Down
21 changes: 18 additions & 3 deletions app/services/payment_provider_customers/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ def initialize(customer)
super(nil)
end

def create(customer_class:, payment_provider_id:, params:)
def create_or_update(customer_class:, payment_provider_id:, params:)
provider_customer = customer_class.find_or_initialize_by(
customer_id: customer.id,
payment_provider_id: payment_provider_id,
)
provider_customer.provider_customer_id = params[:provider_customer_id]
# TODO: Handle settings and create customer on stripe if no customer id

if params.key?(:provider_customer_id)
provider_customer.provider_customer_id = params[:provider_customer_id]
end

provider_customer.save!

result.provider_customer = provider_customer

create_customer_on_provider_service

result
rescue ActiveRecord::RecordInvalid => e
result.fail_with_validations!(e.record)
Expand All @@ -29,5 +34,15 @@ def create(customer_class:, payment_provider_id:, params:)
attr_accessor :customer

delegate :organization, to: :customer

def create_customer_on_provider_service
# NOTE: the customer already exists on the service provider
return if result.provider_customer.provider_customer_id?

# NOTE: organization does not have stripe config or does not enforce customer creation on stripe
return unless organization.stripe_payment_provider&.create_customers

PaymentProviderCustomers::StripeCreateJob.perform_later(result.provider_customer)
end
end
end
62 changes: 62 additions & 0 deletions app/services/payment_provider_customers/stripe_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module PaymentProviderCustomers
class StripeService < BaseService
def initialize(stripe_customer)
@stripe_customer = stripe_customer

super(nil)
end

def create
result.stripe_customer = stripe_customer
return result if stripe_customer.provider_customer_id?

stripe_result = Stripe::Customer.create(
stripe_create_payload,
{ api_key: api_key },
)

stripe_customer.update!(
provider_customer_id: stripe_result.id,
)

result.stripe_customer = stripe_customer
result
end

private

attr_accessor :stripe_customer

delegate :customer, to: :stripe_customer

def organization
customer.organization
end

def api_key
organization.stripe_payment_provider.secret_key
end

def stripe_create_payload
{
address: {
city: customer.city,
country: customer.country,
line1: customer.address_line1,
line2: customer.address_line2,
postal_code: customer.zipcode,
state: customer.state,
},
email: customer.email,
name: customer.name,
metadata: {
lago_customer_id: customer.id,
customer_id: customer.customer_id,
},
phone: customer.phone,
}
end
end
end
1 change: 1 addition & 0 deletions config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ retry: 1
queues:
- default
- clock
- providers
- billing
- webhook

Expand Down
4 changes: 2 additions & 2 deletions spec/factories/payment_provider_customers_factory.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

FactoryBot.define do
factory :payment_provider_customer do
factory :stripe_customer, class: 'PaymentProviderCustomers::StripeCustomer' do
customer

external_customer_id { SecureRandom.uuid }
provider_customer_id { SecureRandom.uuid }
end
end
22 changes: 22 additions & 0 deletions spec/jobs/payment_provider_customers/stripe_create_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PaymentProviderCustomers::StripeCreateJob, type: :job do
let(:stripe_customer) { create(:stripe_customer) }

let(:stripe_service) { instance_double(PaymentProviderCustomers::StripeService) }

it 'calls the stripe create service' do
allow(PaymentProviderCustomers::StripeService).to receive(:new)
.with(stripe_customer)
.and_return(stripe_service)
allow(stripe_service).to receive(:create)
.and_return(BaseService::Result.new)

described_class.perform_now(stripe_customer)

expect(PaymentProviderCustomers::StripeService).to have_received(:new)
expect(stripe_service).to have_received(:create)
end
end
99 changes: 99 additions & 0 deletions spec/services/customers_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,59 @@
end
end
end

context 'when forcing customer creation on stripe' do
before do
create(
:stripe_provider,
organization: organization,
create_customers: true,
)
end

it 'creates a payment provider customer' do
result = customers_service.create_from_api(
organization: organization,
params: create_args,
)

aggregate_failures do
expect(result).to be_success

customer = result.customer
expect(customer.id).to be_present
expect(customer.payment_provider).to eq('stripe')
expect(customer.stripe_customer).to be_present
end
end

context 'when customer is updated' do
before do
create(
:customer,
organization: organization,
customer_id: create_args[:customer_id],
email: '[email protected]',
)
end

it 'does not create a payment provider customer' do
result = customers_service.create_from_api(
organization: organization,
params: create_args,
)

aggregate_failures do
expect(result).to be_success

customer = result.customer
expect(customer.id).to be_present
expect(customer.payment_provider).to be_nil
expect(customer.stripe_customer).not_to be_present
end
end
end
end
end

describe 'create' do
Expand Down Expand Up @@ -201,6 +254,29 @@
expect(result).not_to be_success
end
end

context 'with payment provider' do
before do
create(
:stripe_provider,
organization: organization,
create_customers: true,
)
end

it 'creates a payment provider customer' do
result = customers_service.create(**create_args)

aggregate_failures do
expect(result).to be_success

customer = result.customer
expect(customer.id).to be_present
expect(customer.payment_provider).to eq('stripe')
expect(customer.stripe_customer).to be_present
end
end
end
end

describe 'update' do
Expand Down Expand Up @@ -252,6 +328,29 @@
end
end
end

context 'when updating payment provider' do
let(:update_args) do
{
id: customer.id,
name: 'Updated customer name',
customer_id: customer_id,
payment_provider: 'stripe',
}
end

it 'creates a payment provider customer' do
result = customers_service.update(**update_args)

expect(result).to be_success

updated_customer = result.customer
aggregate_failures do
expect(updated_customer.payment_provider).to eq('stripe')
expect(updated_customer.stripe_customer).to be_present
end
end
end
end

describe 'destroy' do
Expand Down
Loading

0 comments on commit e6cabb8

Please sign in to comment.