Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(customers): Add or update customer currency via GraphQL #463

Merged
merged 2 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/graphql/mutations/customers/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Create < BaseMutation
argument :legal_name, String, required: false
argument :legal_number, String, required: false
argument :vat_rate, Float, required: false
argument :currency, Types::CurrencyEnum, required: false

argument :payment_provider, Types::PaymentProviders::ProviderTypeEnum, required: false
argument :stripe_customer, Types::PaymentProviderCustomers::StripeInput, required: false
Expand Down
3 changes: 2 additions & 1 deletion app/graphql/mutations/customers/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ class Update < BaseMutation
argument :legal_name, String, required: false
argument :legal_number, String, required: false
argument :vat_rate, Float, required: false
argument :payment_provider, Types::PaymentProviders::ProviderTypeEnum, required: false
argument :currency, Types::CurrencyEnum, required: false

argument :payment_provider, Types::PaymentProviders::ProviderTypeEnum, required: false
argument :stripe_customer, Types::PaymentProviderCustomers::StripeInput, required: false

type Types::Customers::Object
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/customers/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Object < Types::BaseObject
field :legal_name, String, null: true
field :legal_number, String, null: true
field :vat_rate, Float, null: true
field :currency, Types::CurrencyEnum, null: true
field :payment_provider, Types::PaymentProviders::ProviderTypeEnum, null: true

field :stripe_customer, Types::PaymentProviderCustomers::Stripe, null: true
Expand Down
2 changes: 1 addition & 1 deletion app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def applicable_vat_rate
end

def default_currency
active_subscription&.plan&.amount_currency
currency || active_subscription&.plan&.amount_currency
end

private
Expand Down
4 changes: 4 additions & 0 deletions app/services/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ def validation_failure!(errors:)
fail_with_error!(ValidationFailure.new(messages: errors))
end

def single_validation_failure!(field:, error_code:)
validation_failure!(errors: { field.to_sym => [error_code] })
end

def throw_error
return if success?

Expand Down
1 change: 1 addition & 0 deletions app/services/customers/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def create(**args)
legal_number: args[:legal_number],
vat_rate: args[:vat_rate],
payment_provider: args[:payment_provider],
currency: args[:currency],
)

# NOTE: handle configuration for configured payment providers
Expand Down
71 changes: 51 additions & 20 deletions app/services/customers/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,33 @@ def update(**args)
customer = result.user.customers.find_by(id: args[:id])
return result.not_found_failure!(resource: 'customer') unless customer

customer.name = args[:name] if args.key?(:name)
customer.country = args[:country]&.upcase if args.key?(:country)
customer.address_line1 = args[:address_line1] if args.key?(:address_line1)
customer.address_line2 = args[:address_line2] if args.key?(:address_line2)
customer.state = args[:state] if args.key?(:state)
customer.zipcode = args[:zipcode] if args.key?(:zipcode)
customer.email = args[:email] if args.key?(:email)
customer.city = args[:city] if args.key?(:city)
customer.url = args[:url] if args.key?(:url)
customer.phone = args[:phone] if args.key?(:phone)
customer.logo_url = args[:logo_url] if args.key?(:logo_url)
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: external_id is not editable if customer is attached to subscriptions
customer.external_id = args[:external_id] if !customer.attached_to_subscriptions? && args.key?(:external_id)

customer.save!
ActiveRecord::Base.transaction do
if args.key?(:currency)
update_currency(customer: customer, currency: args[:currency])
return result unless result.success?
end

customer.name = args[:name] if args.key?(:name)
customer.country = args[:country]&.upcase if args.key?(:country)
customer.address_line1 = args[:address_line1] if args.key?(:address_line1)
customer.address_line2 = args[:address_line2] if args.key?(:address_line2)
customer.state = args[:state] if args.key?(:state)
customer.zipcode = args[:zipcode] if args.key?(:zipcode)
customer.email = args[:email] if args.key?(:email)
customer.city = args[:city] if args.key?(:city)
customer.url = args[:url] if args.key?(:url)
customer.phone = args[:phone] if args.key?(:phone)
customer.logo_url = args[:logo_url] if args.key?(:logo_url)
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: external_id is not editable if customer is attached to subscriptions
customer.external_id = args[:external_id] if !customer.attached_to_subscriptions? && args.key?(:external_id)

customer.save!
end

# NOTE: if payment provider is updated, we need to create/update the provider customer
create_or_update_provider_customer(customer, args[:stripe_customer])
Expand All @@ -36,6 +43,23 @@ def update(**args)
result.record_validation_failure!(record: e.record)
end

def update_currency(customer:, currency:)
result.customer = customer

# TODO: remove default currency check after migration to customer currency
if !editable_currency? && customer.default_currency != currency
return result.single_validation_failure!(
field: :currency,
error_code: 'currencies_does_not_match',
)
end

customer.update!(currency: currency)
result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

private

def create_or_update_provider_customer(customer, billing_configuration = {})
Expand All @@ -54,5 +78,12 @@ def create_or_update_provider_customer(customer, billing_configuration = {})
# NOTE: Create service is modifying an other instance of the provider customer
customer.stripe_customer&.reload
end

def editable_currency?
!result.customer.active_subscription &&
result.customer.applied_add_ons.none? &&
result.customer.applied_coupons.none? &&
result.customer.wallets.none?
end
end
end
3 changes: 3 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,7 @@ input CreateCustomerInput {
"""
clientMutationId: String
country: CountryCode
currency: CurrencyEnum
email: String
externalId: String!
legalName: String
Expand Down Expand Up @@ -2405,6 +2406,7 @@ type Customer {
city: String
country: CountryCode
createdAt: ISO8601DateTime!
currency: CurrencyEnum
email: String
externalId: String!

Expand Down Expand Up @@ -3661,6 +3663,7 @@ input UpdateCustomerInput {
"""
clientMutationId: String
country: CountryCode
currency: CurrencyEnum
email: String
externalId: String!
id: ID!
Expand Down
38 changes: 38 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4761,6 +4761,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "currency",
"description": null,
"type": {
"kind": "ENUM",
"name": "CurrencyEnum",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "paymentProvider",
"description": null,
Expand Down Expand Up @@ -6278,6 +6290,20 @@

]
},
{
"name": "currency",
"description": null,
"type": {
"kind": "ENUM",
"name": "CurrencyEnum",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null,
"args": [

]
},
{
"name": "email",
"description": null,
Expand Down Expand Up @@ -13792,6 +13818,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "currency",
"description": null,
"type": {
"kind": "ENUM",
"name": "CurrencyEnum",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "paymentProvider",
"description": null,
Expand Down
3 changes: 3 additions & 0 deletions spec/graphql/mutations/customers/create_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
country
paymentProvider
stripeCustomer { id, providerCustomerId }
currency
}
}
GQL
Expand All @@ -37,6 +38,7 @@
city: 'London',
country: 'GB',
paymentProvider: 'stripe',
currency: 'EUR',
stripeCustomer: {
providerCustomerId: 'cu_12345',
},
Expand All @@ -52,6 +54,7 @@
expect(result_data['externalId']).to eq('john_doe_2')
expect(result_data['city']).to eq('London')
expect(result_data['country']).to eq('GB')
expect(result_data['currency']).to eq('EUR')
expect(result_data['paymentProvider']).to eq('stripe')
expect(result_data['stripeCustomer']['id']).to be_present
expect(result_data['stripeCustomer']['providerCustomerId']).to eq('cu_12345')
Expand Down
3 changes: 3 additions & 0 deletions spec/graphql/mutations/customers/update_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
name,
externalId
paymentProvider
currency
stripeCustomer { id, providerCustomerId }
}
}
Expand All @@ -35,6 +36,7 @@
name: 'Updated customer',
externalId: external_id,
paymentProvider: 'stripe',
currency: 'EUR',
stripeCustomer: {
providerCustomerId: 'cu_12345',
},
Expand All @@ -49,6 +51,7 @@
expect(result_data['name']).to eq('Updated customer')
expect(result_data['externalId']).to eq(external_id)
expect(result_data['paymentProvider']).to eq('stripe')
expect(result_data['currency']).to eq('EUR')
expect(result_data['stripeCustomer']['id']).to be_present
expect(result_data['stripeCustomer']['providerCustomerId']).to eq('cu_12345')
end
Expand Down
10 changes: 6 additions & 4 deletions spec/services/customers/create_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
customer_id: customer.id,
created_at: customer.created_at,
payment_provider: customer.payment_provider,
organization_id: customer.organization_id
}
organization_id: customer.organization_id,
},
)
end

Expand Down Expand Up @@ -232,6 +232,7 @@
external_id: SecureRandom.uuid,
name: 'Foo Bar',
organization_id: organization.id,
currency: 'EUR',
}
end

Expand All @@ -250,6 +251,7 @@
expect(customer.organization_id).to eq(organization.id)
expect(customer.external_id).to eq(create_args[:external_id])
expect(customer.name).to eq(create_args[:name])
expect(customer.currency).to eq('EUR')
end
end

Expand All @@ -263,8 +265,8 @@
customer_id: customer.id,
created_at: customer.created_at,
payment_provider: customer.payment_provider,
organization_id: customer.organization_id
}
organization_id: customer.organization_id,
},
)
end

Expand Down
20 changes: 20 additions & 0 deletions spec/services/customers/update_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@
expect(updated_customer.external_id).to eq(customer.external_id)
end
end

context 'when updating the currency' do
let(:update_args) do
{
id: customer.id,
currency: 'CAD',
}
end

it 'fails' do
result = customers_service.update(**update_args)

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
expect(result.error.messages.keys).to include(:currency)
expect(result.error.messages[:currency]).to include('currencies_does_not_match')
end
end
end
end

context 'when updating payment provider' do
Expand Down