diff --git a/app/graphql/mutations/customer_portal/update_customer.rb b/app/graphql/mutations/customer_portal/update_customer.rb new file mode 100644 index 00000000000..de30e8c292c --- /dev/null +++ b/app/graphql/mutations/customer_portal/update_customer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module CustomerPortal + class UpdateCustomer < BaseMutation + include AuthenticableCustomerPortalUser + + graphql_name "UpdateCustomerPortalCustomer" + description "Update customer data from Customer Portal" + + input_object_class Types::CustomerPortal::Customers::UpdateInput + type Types::CustomerPortal::Customers::Object + + def resolve(**args) + result = ::CustomerPortal::CustomerUpdateService.call( + customer: context[:customer_portal_user], + args: + ) + + result.success? ? result.customer : result_error(result) + end + end + end +end diff --git a/app/graphql/resolvers/customer_portal/customer_resolver.rb b/app/graphql/resolvers/customer_portal/customer_resolver.rb index 6c3806e333d..0d4e4d01531 100644 --- a/app/graphql/resolvers/customer_portal/customer_resolver.rb +++ b/app/graphql/resolvers/customer_portal/customer_resolver.rb @@ -5,9 +5,9 @@ module CustomerPortal class CustomerResolver < Resolvers::BaseResolver include AuthenticableCustomerPortalUser - description 'Query a customer portal user' + description "Query a customer portal user" - type Types::Customers::Object, null: true + type Types::CustomerPortal::Customers::Object, null: true def resolve context[:customer_portal_user] diff --git a/app/graphql/types/customer_portal/customers/object.rb b/app/graphql/types/customer_portal/customers/object.rb new file mode 100644 index 00000000000..1f208b3b0d8 --- /dev/null +++ b/app/graphql/types/customer_portal/customers/object.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Types + module CustomerPortal + module Customers + class Object < Types::BaseObject + graphql_name "CustomerPortalCustomer" + + field :id, ID, null: false + + field :currency, Types::CurrencyEnum, null: true + field :display_name, String, null: false + field :email, String, null: true + field :firstname, String + field :lastname, String + field :legal_name, String, null: true + field :legal_number, String, null: true + field :name, String + field :tax_identification_number, String, null: true + + field :billing_configuration, Types::Customers::BillingConfiguration, null: true + + # Billing address + field :address_line1, String, null: true + field :address_line2, String, null: true + field :city, String, null: true + field :country, Types::CountryCodeEnum, null: true + field :state, String, null: true + field :zipcode, String, null: true + + field :shipping_address, Types::Customers::Address, null: true + + def billing_configuration + { + id: "#{object&.id}-c0nf", + document_locale: object&.document_locale + } + end + end + end + end +end diff --git a/app/graphql/types/customer_portal/customers/update_input.rb b/app/graphql/types/customer_portal/customers/update_input.rb new file mode 100644 index 00000000000..a1acf2d2c62 --- /dev/null +++ b/app/graphql/types/customer_portal/customers/update_input.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module CustomerPortal + module Customers + class UpdateInput < BaseInputObject + graphql_name "UpdateCustomerPortalCustomerInput" + description "Customer Portal Customer Update input arguments" + + argument :document_locale, String, required: false + argument :email, String, required: false + argument :firstname, String, required: false + argument :lastname, String, required: false + argument :legal_name, String, required: false + argument :name, String, required: false + argument :tax_identification_number, String, required: false + + # Billing address + argument :address_line1, String, required: false + argument :address_line2, String, required: false + argument :city, String, required: false + argument :country, Types::CountryCodeEnum, required: false + argument :state, String, required: false + argument :zipcode, String, required: false + + argument :shipping_address, Types::Customers::AddressInput, required: false + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e126991324a..2ad75e99b9d 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -25,6 +25,7 @@ class MutationType < Types::BaseObject field :download_customer_portal_invoice, mutation: Mutations::CustomerPortal::DownloadInvoice field :generate_customer_portal_url, mutation: Mutations::CustomerPortal::GenerateUrl + field :update_customer_portal_customer, mutation: Mutations::CustomerPortal::UpdateCustomer field :create_invoices_data_export, mutation: Mutations::DataExports::Invoices::Create diff --git a/app/services/customer_portal/customer_update_service.rb b/app/services/customer_portal/customer_update_service.rb new file mode 100644 index 00000000000..7cd775fb08e --- /dev/null +++ b/app/services/customer_portal/customer_update_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module CustomerPortal + class CustomerUpdateService < BaseService + def initialize(customer:, args:) + @customer = customer + @args = args + + super + end + + def call + return result.not_found_failure!(resource: "customer") unless customer + + ActiveRecord::Base.transaction do + customer.name = args[:name] if args.key?(:name) + customer.firstname = args[:firstname] if args.key?(:firstname) + customer.lastname = args[:lastname] if args.key?(:lastname) + customer.legal_name = args[:legal_name] if args.key?(:legal_name) + customer.tax_identification_number = args[:tax_identification_number] if args.key?(:tax_identification_number) + customer.email = args[:email] if args.key?(:email) + + customer.document_locale = args[:document_locale] if args.key?(:document_locale) + + customer.address_line1 = args[:address_line1] if args.key?(:address_line1) + customer.address_line2 = args[:address_line2] if args.key?(:address_line2) + customer.zipcode = args[:zipcode] if args.key?(:zipcode) + customer.city = args[:city] if args.key?(:city) + customer.state = args[:state] if args.key?(:state) + customer.country = args[:country]&.upcase if args.key?(:country) + + shipping_address = args[:shipping_address]&.to_h || {} + customer.shipping_address_line1 = shipping_address[:address_line1] if shipping_address.key?(:address_line1) + customer.shipping_address_line2 = shipping_address[:address_line2] if shipping_address.key?(:address_line2) + customer.shipping_zipcode = shipping_address[:zipcode] if shipping_address.key?(:zipcode) + customer.shipping_city = shipping_address[:city] if shipping_address.key?(:city) + customer.shipping_state = shipping_address[:state] if shipping_address.key?(:state) + customer.shipping_country = shipping_address[:country]&.upcase if shipping_address.key?(:country) + + customer.save! + customer.reload + + tax_codes = [] + if customer.organization.eu_tax_management + # This service does not return a 'result' object but a string + tax_codes << Customers::EuAutoTaxesService.call(customer:) + end + + if tax_codes.present? + taxes_result = Customers::ApplyTaxesService.call(customer:, tax_codes:) + taxes_result.raise_if_error! + end + end + + result.customer = customer + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + rescue BaseService::FailedResult => e + e.result + end + + private + + attr_reader :customer, :args + end +end diff --git a/schema.graphql b/schema.graphql index 67b837b0c7f..c1241009498 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3218,6 +3218,27 @@ input CustomerMetadataInput { value: String! } +type CustomerPortalCustomer { + addressLine1: String + addressLine2: String + billingConfiguration: CustomerBillingConfiguration + city: String + country: CountryCode + currency: CurrencyEnum + displayName: String! + email: String + firstname: String + id: ID! + lastname: String + legalName: String + legalNumber: String + name: String + shippingAddress: CustomerAddress + state: String + taxIdentificationNumber: String + zipcode: String +} + enum CustomerTypeEnum { company individual @@ -5275,6 +5296,16 @@ type Mutation { input: UpdateCustomerInvoiceGracePeriodInput! ): Customer + """ + Update customer data from Customer Portal + """ + updateCustomerPortalCustomer( + """ + Parameters for UpdateCustomerPortalCustomer + """ + input: UpdateCustomerPortalCustomerInput! + ): CustomerPortalCustomer + """ Updates a new Customer Wallet """ @@ -5971,7 +6002,7 @@ type Query { """ Query a customer portal user """ - customerPortalUser: Customer + customerPortalUser: CustomerPortalCustomer """ Query the usage of the customer on the current billing period @@ -7569,6 +7600,31 @@ input UpdateCustomerInvoiceGracePeriodInput { invoiceGracePeriod: Int } +""" +Customer Portal Customer Update input arguments +""" +input UpdateCustomerPortalCustomerInput { + addressLine1: String + addressLine2: String + city: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + country: CountryCode + documentLocale: String + email: String + firstname: String + lastname: String + legalName: String + name: String + shippingAddress: CustomerAddressInput + state: String + taxIdentificationNumber: String + zipcode: String +} + """ Update Wallet Input """ diff --git a/schema.json b/schema.json index 3e821876ab2..88e1ff7565f 100644 --- a/schema.json +++ b/schema.json @@ -13511,6 +13511,279 @@ ], "enumValues": null }, + { + "kind": "OBJECT", + "name": "CustomerPortalCustomer", + "description": null, + "interfaces": [ + + ], + "possibleTypes": null, + "fields": [ + { + "name": "addressLine1", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "addressLine2", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "billingConfiguration", + "description": null, + "type": { + "kind": "OBJECT", + "name": "CustomerBillingConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "city", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "country", + "description": null, + "type": { + "kind": "ENUM", + "name": "CountryCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "currency", + "description": null, + "type": { + "kind": "ENUM", + "name": "CurrencyEnum", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "displayName", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "email", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "firstname", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "lastname", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "legalName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "legalNumber", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "shippingAddress", + "description": null, + "type": { + "kind": "OBJECT", + "name": "CustomerAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "state", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "taxIdentificationNumber", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "zipcode", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + } + ], + "inputFields": null, + "enumValues": null + }, { "kind": "ENUM", "name": "CustomerTypeEnum", @@ -24424,6 +24697,35 @@ } ] }, + { + "name": "updateCustomerPortalCustomer", + "description": "Update customer data from Customer Portal", + "type": { + "kind": "OBJECT", + "name": "CustomerPortalCustomer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateCustomerPortalCustomer", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateCustomerPortalCustomerInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, { "name": "updateCustomerWallet", "description": "Updates a new Customer Wallet", @@ -29794,7 +30096,7 @@ "description": "Query a customer portal user", "type": { "kind": "OBJECT", - "name": "Customer", + "name": "CustomerPortalCustomer", "ofType": null }, "isDeprecated": false, @@ -36353,6 +36655,197 @@ ], "enumValues": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateCustomerPortalCustomerInput", + "description": "Customer Portal Customer Update input arguments", + "interfaces": null, + "possibleTypes": null, + "fields": null, + "inputFields": [ + { + "name": "documentLocale", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstname", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastname", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legalName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxIdentificationNumber", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "addressLine1", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "addressLine2", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "city", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "country", + "description": null, + "type": { + "kind": "ENUM", + "name": "CountryCode", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "zipcode", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shippingAddress", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "CustomerAddressInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "enumValues": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateCustomerWalletInput", diff --git a/spec/graphql/mutations/customer_portal/update_customer_spec.rb b/spec/graphql/mutations/customer_portal/update_customer_spec.rb new file mode 100644 index 00000000000..a6339c11fe6 --- /dev/null +++ b/spec/graphql/mutations/customer_portal/update_customer_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Mutations::CustomerPortal::UpdateCustomer, type: :graphql do + subject(:result) do + execute_graphql( + customer_portal_user: customer, + query: mutation, + variables: { + input: + } + ) + end + + let(:customer) { create(:customer, legal_name: nil) } + + let(:mutation) do + <<~GQL + mutation($input: UpdateCustomerPortalCustomerInput!) { + updateCustomerPortalCustomer(input: $input) { + name + firstname + lastname + legalName + taxIdentificationNumber + email + addressLine1 + addressLine2 + zipcode + city + state + country + billingConfiguration { + documentLocale + } + shippingAddress { + addressLine1 + addressLine2 + zipcode + city + state + country + } + } + } + GQL + end + + let(:input) do + { + name: "Updated customer name", + firstname: "Updated customer firstname", + lastname: "Updated customer lastname", + legalName: "Updated customer legalName", + taxIdentificationNumber: "2246", + email: "customer@email.test", + documentLocale: "fr", + addressLine1: "Updated customer addressLine1", + addressLine2: "Updated customer addressLine2", + zipcode: "Updated customer zipcode", + city: "Updated customer city", + state: "Updated customer state", + country: "PT", + shippingAddress: { + addressLine1: "Updated customer shipping addressLine1", + addressLine2: "Updated customer shipping addressLine2", + zipcode: "Updated customer shipping zipcode", + city: "Updated customer shipping city", + state: "Updated customer shipping state", + country: "ES" + } + } + end + + it_behaves_like "requires a customer portal user" + + it "updates a customer", :aggregate_failures do + result_data = result["data"]["updateCustomerPortalCustomer"] + + expect(result_data["name"]).to eq(input[:name]) + expect(result_data["firstname"]).to eq(input[:firstname]) + expect(result_data["lastname"]).to eq(input[:lastname]) + expect(result_data["taxIdentificationNumber"]).to eq(input[:taxIdentificationNumber]) + expect(result_data["legalName"]).to eq(input[:legalName]) + expect(result_data["email"]).to eq(input[:email]) + expect(result_data["addressLine1"]).to eq(input[:addressLine1]) + expect(result_data["addressLine2"]).to eq(input[:addressLine2]) + expect(result_data["zipcode"]).to eq(input[:zipcode]) + expect(result_data["city"]).to eq(input[:city]) + expect(result_data["state"]).to eq(input[:state]) + expect(result_data["country"]).to eq(input[:country]) + expect(result_data["billingConfiguration"]["documentLocale"]).to eq(input[:documentLocale]) + expect(result_data["shippingAddress"]["addressLine1"]).to eq(input[:shippingAddress][:addressLine1]) + expect(result_data["shippingAddress"]["addressLine2"]).to eq(input[:shippingAddress][:addressLine2]) + expect(result_data["shippingAddress"]["zipcode"]).to eq(input[:shippingAddress][:zipcode]) + expect(result_data["shippingAddress"]["city"]).to eq(input[:shippingAddress][:city]) + expect(result_data["shippingAddress"]["state"]).to eq(input[:shippingAddress][:state]) + expect(result_data["shippingAddress"]["country"]).to eq(input[:shippingAddress][:country]) + end + + context "when updating some fields" do + let(:input) { {name: "Updated customer name"} } + + it "does not change the fields not changed" do + old_firstname = customer.firstname + + result_data = result["data"]["updateCustomerPortalCustomer"] + + expect(result_data["name"]).to eq(input[:name]) + expect(result_data["firstname"]).to eq(old_firstname) + end + end + + context "when updating not allowed fields" do + let(:input) { {currency: "USD"} } + + it "does not change the fields not changed" do + expect { result }.not_to change { customer.reload.currency } + end + end +end diff --git a/spec/graphql/types/customer_portal/customers/object_spec.rb b/spec/graphql/types/customer_portal/customers/object_spec.rb new file mode 100644 index 00000000000..62610dbdb50 --- /dev/null +++ b/spec/graphql/types/customer_portal/customers/object_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Types::CustomerPortal::Customers::Object do + subject { described_class } + + it { is_expected.to have_field(:id).of_type("ID!") } + + it { is_expected.to have_field(:display_name).of_type("String!") } + it { is_expected.to have_field(:firstname).of_type("String") } + it { is_expected.to have_field(:lastname).of_type("String") } + it { is_expected.to have_field(:name).of_type("String") } + it { is_expected.to have_field(:email).of_type("String") } + it { is_expected.to have_field(:legal_name).of_type("String") } + it { is_expected.to have_field(:legal_number).of_type("String") } + it { is_expected.to have_field(:tax_identification_number).of_type("String") } + + it { is_expected.to have_field(:address_line1).of_type("String") } + it { is_expected.to have_field(:address_line2).of_type("String") } + it { is_expected.to have_field(:city).of_type("String") } + it { is_expected.to have_field(:country).of_type("CountryCode") } + it { is_expected.to have_field(:state).of_type("String") } + it { is_expected.to have_field(:zipcode).of_type("String") } + + it { is_expected.to have_field(:shipping_address).of_type("CustomerAddress") } + + it { is_expected.to have_field(:billing_configuration).of_type('CustomerBillingConfiguration') } +end diff --git a/spec/graphql/types/customer_portal/customers/update_input_spec.rb b/spec/graphql/types/customer_portal/customers/update_input_spec.rb new file mode 100644 index 00000000000..96764380aef --- /dev/null +++ b/spec/graphql/types/customer_portal/customers/update_input_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Types::CustomerPortal::Customers::UpdateInput do + subject { described_class } + + it { is_expected.to accept_argument(:document_locale).of_type("String") } + it { is_expected.to accept_argument(:email).of_type("String") } + it { is_expected.to accept_argument(:firstname).of_type("String") } + it { is_expected.to accept_argument(:lastname).of_type("String") } + it { is_expected.to accept_argument(:legal_name).of_type("String") } + it { is_expected.to accept_argument(:name).of_type("String") } + it { is_expected.to accept_argument(:tax_identification_number).of_type("String") } + + it { is_expected.to accept_argument(:address_line1).of_type("String") } + it { is_expected.to accept_argument(:address_line2).of_type("String") } + it { is_expected.to accept_argument(:city).of_type("String") } + it { is_expected.to accept_argument(:country).of_type("CountryCode") } + it { is_expected.to accept_argument(:state).of_type("String") } + it { is_expected.to accept_argument(:zipcode).of_type("String") } + + it { is_expected.to accept_argument(:shipping_address).of_type("CustomerAddressInput") } +end diff --git a/spec/services/customer_portal/customer_update_service_spec.rb b/spec/services/customer_portal/customer_update_service_spec.rb new file mode 100644 index 00000000000..e15b69c7f9a --- /dev/null +++ b/spec/services/customer_portal/customer_update_service_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CustomerPortal::CustomerUpdateService, type: :service do + subject(:result) { described_class.call(customer:, args: update_args) } + + let(:customer) { create :customer } + + let(:update_args) do + { + document_locale: "es", + name: "Updated customer name", + firstname: "Updated customer firstname", + lastname: "Updated customer lastname", + legal_name: "Updated customer legal_name", + tax_identification_number: "2246", + email: "customer@email.test", + address_line1: "Updated customer address line1", + address_line2: "Updated customer address line2", + zipcode: "Updated customer zipcode", + city: "Updated customer city", + state: "Updated customer state", + country: "PT", + shipping_address: { + address_line1: "Updated customer shipping address line1", + address_line2: "Updated customer shipping address line2", + zipcode: "Updated customer shipping zipcode", + city: "Updated customer shipping city", + state: "Updated customer shipping state", + country: "Updated customer shipping country" + } + } + end + + it "updates the customer", :aggregate_failures do + expect(result).to be_success + + updated_customer = result.customer + + expect(updated_customer.name).to eq(update_args[:name]) + expect(updated_customer.firstname).to eq(update_args[:firstname]) + expect(updated_customer.lastname).to eq(update_args[:lastname]) + expect(updated_customer.legal_name).to eq(update_args[:legal_name]) + expect(updated_customer.tax_identification_number).to eq(update_args[:tax_identification_number]) + expect(updated_customer.email).to eq(update_args[:email]) + expect(updated_customer.document_locale).to eq(update_args[:document_locale]) + + expect(updated_customer.address_line1).to eq(update_args[:address_line1]) + expect(updated_customer.address_line2).to eq(update_args[:address_line2]) + expect(updated_customer.zipcode).to eq(update_args[:zipcode]) + expect(updated_customer.city).to eq(update_args[:city]) + expect(updated_customer.state).to eq(update_args[:state]) + expect(updated_customer.country).to eq(update_args[:country]) + + shipping_address = update_args[:shipping_address] + expect(updated_customer.shipping_address_line1).to eq(shipping_address[:address_line1]) + expect(updated_customer.shipping_address_line2).to eq(shipping_address[:address_line2]) + expect(updated_customer.shipping_zipcode).to eq(shipping_address[:zipcode]) + expect(updated_customer.shipping_city).to eq(shipping_address[:city]) + expect(updated_customer.shipping_state).to eq(shipping_address[:state]) + expect(updated_customer.shipping_country).to eq(shipping_address[:country].upcase) + end + + context "when partialy updating" do + let(:update_args) do + { + name: "Updated customer name", + shipping_address: { + address_line1: "Updated customer shipping address line1" + } + } + end + + it "updates only the updated args", :aggregate_failures do + expect { result }.not_to change { customer.reload.email } + + expect(result).to be_success + expect(result.customer.name).to eq(update_args[:name]) + expect(result.customer.shipping_address_line1).to eq(update_args[:shipping_address][:address_line1]) + end + end + + context "when organization has eu tax management" do + let(:eu_auto_tax_service) { instance_double(Customers::EuAutoTaxesService) } + let(:organization) { customer.organization } + let(:tax_code) { "lago_eu_fr_standard" } + + before do + create(:tax, organization:, code: "lago_eu_fr_standard", rate: 20.0) + organization.update!(eu_tax_management: true) + + allow(Customers::EuAutoTaxesService).to receive(:new).and_return(eu_auto_tax_service) + allow(eu_auto_tax_service).to receive(:call).and_return(tax_code) + end + + it "assigns the right tax to the customer", :aggregate_failures do + expect(result).to be_success + + tax = result.customer.taxes.first + expect(tax.code).to eq(tax_code) + end + + context "when applying taxes fails" do + let(:apply_taxes_result) do + BaseService::Result.new.not_found_failure!(resource: "tax") + end + + before do + allow(Customers::ApplyTaxesService).to receive(:call).and_return(apply_taxes_result) + end + + it "returns a service error" do + expect(result).not_to be_success + expect(result.error.error_code).to eq("tax_not_found") + end + end + end + + context "when customer is not found" do + let(:customer) { nil } + + it "returns an error" do + expect(result).not_to be_success + expect(result.error.error_code).to eq("customer_not_found") + end + end + + context "with validation error" do + let(:update_args) { {country: "invalid country code"} } + + it "returns an error", :aggregate_failures do + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::ValidationFailure) + expect(result.error.messages[:country]).to eq(["not_a_valid_country_code"]) + end + end +end