From 3404ea9ade8a55e9a61759c88161f18359b34fb4 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Thu, 15 Sep 2022 15:11:18 +0200 Subject: [PATCH 1/2] feat(customers): Add or update customer currency via GraphQL --- app/graphql/mutations/customers/create.rb | 1 + app/graphql/mutations/customers/update.rb | 3 +- app/graphql/types/customers/object.rb | 1 + app/models/customer.rb | 2 +- app/services/base_service.rb | 4 ++ app/services/customers/create_service.rb | 1 + app/services/customers/update_service.rb | 70 +++++++++++++------ schema.graphql | 3 + schema.json | 38 ++++++++++ .../mutations/customers/create_spec.rb | 3 + .../mutations/customers/update_spec.rb | 3 + .../services/customers/create_service_spec.rb | 10 +-- .../services/customers/update_service_spec.rb | 20 ++++++ 13 files changed, 133 insertions(+), 26 deletions(-) diff --git a/app/graphql/mutations/customers/create.rb b/app/graphql/mutations/customers/create.rb index 3a3434ec6ab..d3bccf17f83 100644 --- a/app/graphql/mutations/customers/create.rb +++ b/app/graphql/mutations/customers/create.rb @@ -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 diff --git a/app/graphql/mutations/customers/update.rb b/app/graphql/mutations/customers/update.rb index feec4fecdfe..64e40bd7924 100644 --- a/app/graphql/mutations/customers/update.rb +++ b/app/graphql/mutations/customers/update.rb @@ -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 diff --git a/app/graphql/types/customers/object.rb b/app/graphql/types/customers/object.rb index ddc35b06699..e74a46c4c80 100644 --- a/app/graphql/types/customers/object.rb +++ b/app/graphql/types/customers/object.rb @@ -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 diff --git a/app/models/customer.rb b/app/models/customer.rb index 4a469ab58ab..95e6a971bf8 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -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 diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 77ded45c328..f6b4254ffbf 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -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? diff --git a/app/services/customers/create_service.rb b/app/services/customers/create_service.rb index ba2fa0945fe..acf7c41b3d1 100644 --- a/app/services/customers/create_service.rb +++ b/app/services/customers/create_service.rb @@ -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 diff --git a/app/services/customers/update_service.rb b/app/services/customers/update_service.rb index 78a6a7eb033..4163e1e5dd3 100644 --- a/app/services/customers/update_service.rb +++ b/app/services/customers/update_service.rb @@ -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]) @@ -36,6 +43,29 @@ def update(**args) result.record_validation_failure!(record: e.record) end + def update_currency(customer:, currency:) + # TODO: remove default_currency check after currency migration + + result.customer = customer + customer_cuurency = (customer.currency || customer.default_currency) + + if customer_cuurency.present? && customer_cuurency != currency + return result.single_validation_failure!( + field: :currency, + error_code: 'currencies_does_not_match', + ) + end + + if customer_cuurency.nil? + customer.update!(currency: currency) + return result + end + + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + private def create_or_update_provider_customer(customer, billing_configuration = {}) diff --git a/schema.graphql b/schema.graphql index 89e2fc6c942..554c4a29f1b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1611,6 +1611,7 @@ input CreateCustomerInput { """ clientMutationId: String country: CountryCode + currency: CurrencyEnum email: String externalId: String! legalName: String @@ -2405,6 +2406,7 @@ type Customer { city: String country: CountryCode createdAt: ISO8601DateTime! + currency: CurrencyEnum email: String externalId: String! @@ -3661,6 +3663,7 @@ input UpdateCustomerInput { """ clientMutationId: String country: CountryCode + currency: CurrencyEnum email: String externalId: String! id: ID! diff --git a/schema.json b/schema.json index 885012a8d6e..37184b66505 100644 --- a/schema.json +++ b/schema.json @@ -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, @@ -6278,6 +6290,20 @@ ] }, + { + "name": "currency", + "description": null, + "type": { + "kind": "ENUM", + "name": "CurrencyEnum", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "email", "description": null, @@ -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, diff --git a/spec/graphql/mutations/customers/create_spec.rb b/spec/graphql/mutations/customers/create_spec.rb index 2a9a7d34db6..f3a98e49b4a 100644 --- a/spec/graphql/mutations/customers/create_spec.rb +++ b/spec/graphql/mutations/customers/create_spec.rb @@ -18,6 +18,7 @@ country paymentProvider stripeCustomer { id, providerCustomerId } + currency } } GQL @@ -37,6 +38,7 @@ city: 'London', country: 'GB', paymentProvider: 'stripe', + currency: 'EUR', stripeCustomer: { providerCustomerId: 'cu_12345', }, @@ -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') diff --git a/spec/graphql/mutations/customers/update_spec.rb b/spec/graphql/mutations/customers/update_spec.rb index 44771d8ccea..7f16aa14cde 100644 --- a/spec/graphql/mutations/customers/update_spec.rb +++ b/spec/graphql/mutations/customers/update_spec.rb @@ -16,6 +16,7 @@ name, externalId paymentProvider + currency stripeCustomer { id, providerCustomerId } } } @@ -35,6 +36,7 @@ name: 'Updated customer', externalId: external_id, paymentProvider: 'stripe', + currency: 'EUR', stripeCustomer: { providerCustomerId: 'cu_12345', }, @@ -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 diff --git a/spec/services/customers/create_service_spec.rb b/spec/services/customers/create_service_spec.rb index 89b06ad0904..6108edfdcc1 100644 --- a/spec/services/customers/create_service_spec.rb +++ b/spec/services/customers/create_service_spec.rb @@ -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 @@ -232,6 +232,7 @@ external_id: SecureRandom.uuid, name: 'Foo Bar', organization_id: organization.id, + currency: 'EUR', } end @@ -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 @@ -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 diff --git a/spec/services/customers/update_service_spec.rb b/spec/services/customers/update_service_spec.rb index 1b2d7681fc0..853368aea19 100644 --- a/spec/services/customers/update_service_spec.rb +++ b/spec/services/customers/update_service_spec.rb @@ -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 From c413af9ea3b5c3dd64f3fece5bbd63c5dd72ea9c Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Thu, 15 Sep 2022 15:53:31 +0200 Subject: [PATCH 2/2] Check if currency is editable at customer level --- app/services/customers/update_service.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/services/customers/update_service.rb b/app/services/customers/update_service.rb index 4163e1e5dd3..95ed36819fc 100644 --- a/app/services/customers/update_service.rb +++ b/app/services/customers/update_service.rb @@ -44,23 +44,17 @@ def update(**args) end def update_currency(customer:, currency:) - # TODO: remove default_currency check after currency migration - result.customer = customer - customer_cuurency = (customer.currency || customer.default_currency) - if customer_cuurency.present? && customer_cuurency != currency + # 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 - if customer_cuurency.nil? - customer.update!(currency: currency) - return result - end - + customer.update!(currency: currency) result rescue ActiveRecord::RecordInvalid => e result.record_validation_failure!(record: e.record) @@ -84,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