From faa7518058dd2f9a875e716662795a84f4058520 Mon Sep 17 00:00:00 2001 From: Ivan Novosad Date: Mon, 21 Oct 2024 11:47:22 +0200 Subject: [PATCH] Feat hubspot customers services (#2674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Roadmap Task 👉 https://getlago.canny.io/feature-requests/p/integration-with-hubspot ## Context When creating or updating customers in Lago, we must sync them to Hubspot. Depending on if Lago customer is either company or individual, we need to call different services (Nango endpoints). ## Description This PR implements services that handle syncing the customers either as companies or contacts to Hubspot via Nango. --- .../api/v1/customers_controller.rb | 3 +- app/graphql/types/integrations/hubspot.rb | 1 - .../integrations/hubspot/create_input.rb | 1 - .../integrations/hubspot/update_input.rb | 1 - .../save_portal_id_job.rb} | 6 +- app/jobs/send_webhook_job.rb | 6 +- .../integrations/hubspot_integration.rb | 6 +- .../integration_customers/anrok_service.rb | 5 +- .../integration_customers/base_service.rb | 4 + .../integration_customers/create_service.rb | 10 +- app/services/integration_customers/factory.rb | 6 +- .../integration_customers/hubspot_service.rb | 53 + .../integration_customers/netsuite_service.rb | 5 +- .../integration_customers/update_service.rb | 22 +- .../integration_customers/xero_service.rb | 5 +- .../aggregator/account_information_service.rb | 28 + .../integrations/aggregator/base_service.rb | 11 +- .../aggregator/companies/base_service.rb | 36 + .../aggregator/companies/create_service.rb | 55 + .../aggregator/companies/payloads/factory.rb | 25 + .../aggregator/companies/payloads/hubspot.rb | 42 + .../aggregator/companies/update_service.rb | 56 + .../aggregator/contacts/base_service.rb | 25 +- .../aggregator/contacts/create_service.rb | 2 +- .../contacts/payloads/base_payload.rb | 6 + .../aggregator/contacts/payloads/factory.rb | 7 + .../aggregator/contacts/payloads/hubspot.rb | 44 + .../aggregator/contacts/payloads/netsuite.rb | 6 - .../aggregator/contacts/update_service.rb | 8 +- .../aggregator/custom_object_service.rb | 43 + .../send_private_app_token_service.rb | 34 - .../companies/deploy_properties_service.rb | 10 + .../contacts/deploy_properties_service.rb | 10 + .../integrations/hubspot/create_service.rb | 3 +- .../hubspot/invoices/deploy_object_service.rb | 19 +- .../invoices/deploy_properties_service.rb | 1 + .../hubspot/save_portal_id_service.rb | 29 + .../subscriptions/deploy_object_service.rb | 2 + .../deploy_properties_service.rb | 1 + .../integrations/hubspot/update_service.rb | 7 - .../accounting_customer_created_service.rb | 13 + .../accounting_customer_error_service.rb | 17 + .../crm_customer_created_service.rb | 13 + .../crm_customer_error_service.rb | 17 + .../integrations/customer_created_service.rb | 4 - .../integrations/customer_error_service.rb | 8 - .../lago_http_client/client.rb | 3 +- schema.graphql | 3 - schema.json | 46 - spec/factories/integrations.rb | 2 +- .../account_information_response.json | 11 + .../companies/failure_hash_response.json | 20 + .../companies/success_hash_response.json | 16 + .../companies/success_string_response.json | 1 + .../custom_object_response.json | 1310 +++++++++++++++++ .../integrations/hubspot/create_spec.rb | 4 +- .../integrations/hubspot/update_spec.rb | 1 - .../integrations/hubspot/create_input_spec.rb | 1 - .../integrations/hubspot/update_input_spec.rb | 1 - .../types/integrations/hubspot_spec.rb | 1 - .../send_private_app_token_job_spec.rb | 25 - .../hubspot/save_portal_id_job_spec.rb | 25 + spec/jobs/send_webhook_job_spec.rb | 44 + .../integrations/hubspot_integration_spec.rb | 15 +- .../hubspot_service_spec.rb | 50 + .../update_service_spec.rb | 25 +- .../account_information_service_spec.rb | 43 + .../companies/create_service_spec.rb | 377 +++++ .../companies/payloads/factory_spec.rb | 25 + .../companies/payloads/hubspot_spec.rb | 58 + .../companies/update_service_spec.rb | 288 ++++ .../contacts/create_service_spec.rb | 81 + .../contacts/payloads/factory_spec.rb | 10 + .../contacts/payloads/hubspot_spec.rb | 60 + .../aggregator/custom_object_service_spec.rb | 51 + .../send_private_app_token_service_spec.rb | 36 - .../hubspot/create_service_spec.rb | 6 +- .../invoices/deploy_object_service_spec.rb | 56 + .../hubspot/save_portal_id_service_spec.rb | 48 + .../deploy_object_service_spec.rb | 1 + .../hubspot/update_service_spec.rb | 11 +- ...counting_customer_created_service_spec.rb} | 2 +- ...accounting_customer_error_service_spec.rb} | 2 +- .../crm_customer_created_service_spec.rb | 17 + .../crm_customer_error_service_spec.rb | 15 + 85 files changed, 3237 insertions(+), 270 deletions(-) rename app/jobs/integrations/{aggregator/send_private_app_token_job.rb => hubspot/save_portal_id_job.rb} (61%) create mode 100644 app/services/integration_customers/hubspot_service.rb create mode 100644 app/services/integrations/aggregator/account_information_service.rb create mode 100644 app/services/integrations/aggregator/companies/base_service.rb create mode 100644 app/services/integrations/aggregator/companies/create_service.rb create mode 100644 app/services/integrations/aggregator/companies/payloads/factory.rb create mode 100644 app/services/integrations/aggregator/companies/payloads/hubspot.rb create mode 100644 app/services/integrations/aggregator/companies/update_service.rb create mode 100644 app/services/integrations/aggregator/contacts/payloads/hubspot.rb create mode 100644 app/services/integrations/aggregator/custom_object_service.rb delete mode 100644 app/services/integrations/aggregator/send_private_app_token_service.rb create mode 100644 app/services/integrations/hubspot/save_portal_id_service.rb create mode 100644 app/services/webhooks/integrations/accounting_customer_created_service.rb create mode 100644 app/services/webhooks/integrations/accounting_customer_error_service.rb create mode 100644 app/services/webhooks/integrations/crm_customer_created_service.rb create mode 100644 app/services/webhooks/integrations/crm_customer_error_service.rb create mode 100644 spec/fixtures/integration_aggregator/account_information_response.json create mode 100644 spec/fixtures/integration_aggregator/companies/failure_hash_response.json create mode 100644 spec/fixtures/integration_aggregator/companies/success_hash_response.json create mode 100644 spec/fixtures/integration_aggregator/companies/success_string_response.json create mode 100644 spec/fixtures/integration_aggregator/custom_object_response.json delete mode 100644 spec/jobs/integrations/aggregator/send_private_app_token_job_spec.rb create mode 100644 spec/jobs/integrations/hubspot/save_portal_id_job_spec.rb create mode 100644 spec/services/integration_customers/hubspot_service_spec.rb create mode 100644 spec/services/integrations/aggregator/account_information_service_spec.rb create mode 100644 spec/services/integrations/aggregator/companies/create_service_spec.rb create mode 100644 spec/services/integrations/aggregator/companies/payloads/factory_spec.rb create mode 100644 spec/services/integrations/aggregator/companies/payloads/hubspot_spec.rb create mode 100644 spec/services/integrations/aggregator/companies/update_service_spec.rb create mode 100644 spec/services/integrations/aggregator/contacts/payloads/hubspot_spec.rb create mode 100644 spec/services/integrations/aggregator/custom_object_service_spec.rb delete mode 100644 spec/services/integrations/aggregator/send_private_app_token_service_spec.rb create mode 100644 spec/services/integrations/hubspot/save_portal_id_service_spec.rb rename spec/services/webhooks/integrations/{customer_created_service_spec.rb => accounting_customer_created_service_spec.rb} (84%) rename spec/services/webhooks/integrations/{customer_error_service_spec.rb => accounting_customer_error_service_spec.rb} (86%) create mode 100644 spec/services/webhooks/integrations/crm_customer_created_service_spec.rb create mode 100644 spec/services/webhooks/integrations/crm_customer_error_service_spec.rb diff --git a/app/controllers/api/v1/customers_controller.rb b/app/controllers/api/v1/customers_controller.rb index a883c624e73..9fdbd650c6d 100644 --- a/app/controllers/api/v1/customers_controller.rb +++ b/app/controllers/api/v1/customers_controller.rb @@ -121,7 +121,8 @@ def create_params :integration_type, :integration_code, :subsidiary_id, - :sync_with_provider + :sync_with_provider, + :targeted_object ], billing_configuration: [ :invoice_grace_period, diff --git a/app/graphql/types/integrations/hubspot.rb b/app/graphql/types/integrations/hubspot.rb index 90d34105faa..20a85cbcfe1 100644 --- a/app/graphql/types/integrations/hubspot.rb +++ b/app/graphql/types/integrations/hubspot.rb @@ -10,7 +10,6 @@ class Hubspot < Types::BaseObject field :default_targeted_object, Types::Integrations::Hubspot::TargetedObjectsEnum, null: false field :id, ID, null: false field :name, String, null: false - field :private_app_token, String, null: false field :sync_invoices, Boolean field :sync_subscriptions, Boolean end diff --git a/app/graphql/types/integrations/hubspot/create_input.rb b/app/graphql/types/integrations/hubspot/create_input.rb index 6a773528b92..7624e0639b3 100644 --- a/app/graphql/types/integrations/hubspot/create_input.rb +++ b/app/graphql/types/integrations/hubspot/create_input.rb @@ -11,7 +11,6 @@ class CreateInput < Types::BaseInputObject argument :connection_id, String, required: true argument :default_targeted_object, Types::Integrations::Hubspot::TargetedObjectsEnum, required: true - argument :private_app_token, String, required: true argument :sync_invoices, Boolean, required: false argument :sync_subscriptions, Boolean, required: false end diff --git a/app/graphql/types/integrations/hubspot/update_input.rb b/app/graphql/types/integrations/hubspot/update_input.rb index 6c9df5a16ad..ea99614b4dd 100644 --- a/app/graphql/types/integrations/hubspot/update_input.rb +++ b/app/graphql/types/integrations/hubspot/update_input.rb @@ -13,7 +13,6 @@ class UpdateInput < Types::BaseInputObject argument :connection_id, String, required: false argument :default_targeted_object, Types::Integrations::Hubspot::TargetedObjectsEnum, required: false - argument :private_app_token, String, required: false argument :sync_invoices, Boolean, required: false argument :sync_subscriptions, Boolean, required: false end diff --git a/app/jobs/integrations/aggregator/send_private_app_token_job.rb b/app/jobs/integrations/hubspot/save_portal_id_job.rb similarity index 61% rename from app/jobs/integrations/aggregator/send_private_app_token_job.rb rename to app/jobs/integrations/hubspot/save_portal_id_job.rb index 12963d57720..17f515a1887 100644 --- a/app/jobs/integrations/aggregator/send_private_app_token_job.rb +++ b/app/jobs/integrations/hubspot/save_portal_id_job.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true module Integrations - module Aggregator - class SendPrivateAppTokenJob < ApplicationJob + module Hubspot + class SavePortalIdJob < ApplicationJob queue_as 'integrations' retry_on LagoHttpClient::HttpError, wait: :polynomially_longer, attempts: 3 def perform(integration:) - result = Integrations::Aggregator::SendPrivateAppTokenService.call(integration:) + result = Integrations::Hubspot::SavePortalIdService.call(integration:) result.raise_if_error! end end diff --git a/app/jobs/send_webhook_job.rb b/app/jobs/send_webhook_job.rb index 497095d5298..c569a978e99 100644 --- a/app/jobs/send_webhook_job.rb +++ b/app/jobs/send_webhook_job.rb @@ -23,8 +23,10 @@ class SendWebhookJob < ApplicationJob 'events.errors' => Webhooks::Events::ValidationErrorsService, 'fee.created' => Webhooks::Fees::PayInAdvanceCreatedService, 'fee.tax_provider_error' => Webhooks::Integrations::Taxes::FeeErrorService, - 'customer.accounting_provider_created' => Webhooks::Integrations::CustomerCreatedService, - 'customer.accounting_provider_error' => Webhooks::Integrations::CustomerErrorService, + 'customer.accounting_provider_created' => Webhooks::Integrations::AccountingCustomerCreatedService, + 'customer.accounting_provider_error' => Webhooks::Integrations::AccountingCustomerErrorService, + 'customer.crm_provider_created' => Webhooks::Integrations::CrmCustomerCreatedService, + 'customer.crm_provider_error' => Webhooks::Integrations::CrmCustomerErrorService, 'customer.payment_provider_created' => Webhooks::PaymentProviders::CustomerCreatedService, 'customer.payment_provider_error' => Webhooks::PaymentProviders::CustomerErrorService, 'customer.checkout_url_generated' => Webhooks::PaymentProviders::CustomerCheckoutService, diff --git a/app/models/integrations/hubspot_integration.rb b/app/models/integrations/hubspot_integration.rb index fe5058915ae..42c3d6d1dd7 100644 --- a/app/models/integrations/hubspot_integration.rb +++ b/app/models/integrations/hubspot_integration.rb @@ -2,12 +2,12 @@ module Integrations class HubspotIntegration < BaseIntegration - validates :connection_id, :private_app_token, :default_targeted_object, presence: true + validates :connection_id, :default_targeted_object, presence: true settings_accessors :default_targeted_object, :sync_subscriptions, :sync_invoices, :subscriptions_object_type_id, :invoices_object_type_id, :companies_properties_version, :contacts_properties_version, - :subscriptions_properties_version, :invoices_properties_version - secrets_accessors :connection_id, :private_app_token + :subscriptions_properties_version, :invoices_properties_version, :portal_id + secrets_accessors :connection_id TARGETED_OBJECTS = %w[companies contacts].freeze diff --git a/app/services/integration_customers/anrok_service.rb b/app/services/integration_customers/anrok_service.rb index e5f1eae91fe..b73101dca20 100644 --- a/app/services/integration_customers/anrok_service.rb +++ b/app/services/integration_customers/anrok_service.rb @@ -2,10 +2,11 @@ module IntegrationCustomers class AnrokService < ::BaseService - def initialize(integration:, customer:, subsidiary_id:) + def initialize(integration:, customer:, subsidiary_id:, **params) @customer = customer @subsidiary_id = subsidiary_id @integration = integration + @params = params&.with_indifferent_access super(nil) end @@ -26,6 +27,6 @@ def create private - attr_reader :integration, :customer, :subsidiary_id + attr_reader :integration, :customer, :subsidiary_id, :params end end diff --git a/app/services/integration_customers/base_service.rb b/app/services/integration_customers/base_service.rb index 57fd476bedd..3f15c43a075 100644 --- a/app/services/integration_customers/base_service.rb +++ b/app/services/integration_customers/base_service.rb @@ -30,6 +30,10 @@ def subsidiary_id @subsidiary_id ||= params[:subsidiary_id] end + def targeted_object + @targeted_object ||= params[:targeted_object] + end + def external_customer_id @external_customer_id ||= params[:external_customer_id] end diff --git a/app/services/integration_customers/create_service.rb b/app/services/integration_customers/create_service.rb index 5472da006ff..f81fc069976 100644 --- a/app/services/integration_customers/create_service.rb +++ b/app/services/integration_customers/create_service.rb @@ -26,7 +26,9 @@ def call attr_reader :customer def sync_customer! - integration_customer_service = IntegrationCustomers::Factory.new_instance(integration:, customer:, subsidiary_id:) + integration_customer_service = IntegrationCustomers::Factory.new_instance( + integration:, customer:, subsidiary_id:, **params + ) return result unless integration_customer_service @@ -49,6 +51,12 @@ def link_customer! if integration&.type&.to_s == 'Integrations::NetsuiteIntegration' new_integration_customer.subsidiary_id = subsidiary_id + new_integration_customer.save! + end + + if integration&.type&.to_s == 'Integrations::HubspotIntegration' + new_integration_customer.targeted_object = targeted_object + new_integration_customer.save! end result.integration_customer = new_integration_customer diff --git a/app/services/integration_customers/factory.rb b/app/services/integration_customers/factory.rb index 57e27bfcd64..3a23d578c23 100644 --- a/app/services/integration_customers/factory.rb +++ b/app/services/integration_customers/factory.rb @@ -2,8 +2,8 @@ module IntegrationCustomers class Factory - def self.new_instance(integration:, customer:, subsidiary_id:) - service_class(integration).new(integration:, customer:, subsidiary_id:) + def self.new_instance(integration:, customer:, subsidiary_id:, **params) + service_class(integration).new(integration:, customer:, subsidiary_id:, **params) end def self.service_class(integration) @@ -14,6 +14,8 @@ def self.service_class(integration) IntegrationCustomers::AnrokService when 'Integrations::XeroIntegration' IntegrationCustomers::XeroService + when 'Integrations::HubspotIntegration' + IntegrationCustomers::HubspotService else raise(NotImplementedError) end diff --git a/app/services/integration_customers/hubspot_service.rb b/app/services/integration_customers/hubspot_service.rb new file mode 100644 index 00000000000..b201908519f --- /dev/null +++ b/app/services/integration_customers/hubspot_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module IntegrationCustomers + class HubspotService < ::BaseService + def initialize(integration:, customer:, subsidiary_id:, **params) + @customer = customer + @subsidiary_id = subsidiary_id + @integration = integration + @params = params&.with_indifferent_access + + super(nil) + end + + def create + create_result = create_service_class.call( + integration:, + customer:, + subsidiary_id: nil + ) + + return create_result if create_result.error + + new_integration_customer = IntegrationCustomers::BaseCustomer.create!( + integration:, + customer:, + external_customer_id: create_result.contact_id, + email: create_result.email, + type: 'IntegrationCustomers::HubspotCustomer', + sync_with_provider: true, + targeted_object: + ) + + result.integration_customer = new_integration_customer + result + end + + private + + attr_reader :integration, :customer, :subsidiary_id, :params + + def create_service_class + @create_service_class ||= if targeted_object == 'contacts' + Integrations::Aggregator::Contacts::CreateService + else + Integrations::Aggregator::Companies::CreateService + end + end + + def targeted_object + @targeted_object ||= params[:targeted_object] + end + end +end diff --git a/app/services/integration_customers/netsuite_service.rb b/app/services/integration_customers/netsuite_service.rb index f7be02c3570..3200a706ab1 100644 --- a/app/services/integration_customers/netsuite_service.rb +++ b/app/services/integration_customers/netsuite_service.rb @@ -2,10 +2,11 @@ module IntegrationCustomers class NetsuiteService < ::BaseService - def initialize(integration:, customer:, subsidiary_id:) + def initialize(integration:, customer:, subsidiary_id:, **params) @customer = customer @subsidiary_id = subsidiary_id @integration = integration + @params = params&.with_indifferent_access super(nil) end @@ -29,6 +30,6 @@ def create private - attr_reader :integration, :customer, :subsidiary_id + attr_reader :integration, :customer, :subsidiary_id, :params end end diff --git a/app/services/integration_customers/update_service.rb b/app/services/integration_customers/update_service.rb index b89e90fd195..121a00fb431 100644 --- a/app/services/integration_customers/update_service.rb +++ b/app/services/integration_customers/update_service.rb @@ -14,15 +14,13 @@ def call return result if integration_customer.type == 'IntegrationCustomers::AnrokCustomer' return result.not_found_failure!(resource: 'integration_customer') unless integration_customer - integration_customer.update!(external_customer_id:) if external_customer_id.present? + integration_customer.external_customer_id = external_customer_id if external_customer_id.present? + integration_customer.targeted_object = targeted_object if targeted_object.present? + integration_customer.save! - if sync_with_provider - integration_customer.subsidiary_id = subsidiary_id if subsidiary_id.present? - - update_result = Integrations::Aggregator::Contacts::UpdateService.call(integration:, integration_customer:) + if integration_customer.external_customer_id.present? + update_result = update_service_class.call(integration:, integration_customer:) return update_result unless update_result.success? - - integration_customer.save! end result.integration_customer = integration_customer @@ -34,5 +32,15 @@ def call attr_reader :integration_customer delegate :customer, to: :integration_customer + + def update_service_class + @update_service_class ||= if integration_customer.type != 'IntegrationCustomers::HubspotCustomer' + Integrations::Aggregator::Contacts::UpdateService + elsif integration_customer.targeted_object == 'contacts' + Integrations::Aggregator::Contacts::UpdateService + else + Integrations::Aggregator::Companies::UpdateService + end + end end end diff --git a/app/services/integration_customers/xero_service.rb b/app/services/integration_customers/xero_service.rb index 96d7c5edf1d..d30c63dae13 100644 --- a/app/services/integration_customers/xero_service.rb +++ b/app/services/integration_customers/xero_service.rb @@ -2,10 +2,11 @@ module IntegrationCustomers class XeroService < ::BaseService - def initialize(integration:, customer:, subsidiary_id:) + def initialize(integration:, customer:, subsidiary_id:, **params) @customer = customer @subsidiary_id = subsidiary_id @integration = integration + @params = params&.with_indifferent_access super(nil) end @@ -33,6 +34,6 @@ def create private - attr_reader :integration, :customer, :subsidiary_id + attr_reader :integration, :customer, :subsidiary_id, :params end end diff --git a/app/services/integrations/aggregator/account_information_service.rb b/app/services/integrations/aggregator/account_information_service.rb new file mode 100644 index 00000000000..14901963e2e --- /dev/null +++ b/app/services/integrations/aggregator/account_information_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + class AccountInformationService < BaseService + def action_path + 'v1/account-information' + end + + def call + response = http_client.get(headers:) + + result.account_information = OpenStruct.new(response) + result + end + + private + + def headers + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{secret_key}", + 'Provider-Config-Key' => provider_key + } + end + end + end +end diff --git a/app/services/integrations/aggregator/base_service.rb b/app/services/integrations/aggregator/base_service.rb index fca83c4a0e4..bd058218bb7 100644 --- a/app/services/integrations/aggregator/base_service.rb +++ b/app/services/integrations/aggregator/base_service.rb @@ -67,7 +67,7 @@ def headers def deliver_error_webhook(customer:, code:, message:) SendWebhookJob.perform_later( - 'customer.accounting_provider_error', + error_webhook_code, customer, provider:, provider_code: integration.code, @@ -108,6 +108,15 @@ def secret_key ENV['NANGO_SECRET_KEY'] end + def error_webhook_code + case provider + when 'hubspot' + 'customer.crm_provider_error' + else + 'customer.accounting_provider_error' + end + end + def code(error) json = error.json_message json['type'].presence || json.dig('error', 'payload', 'name').presence || json.dig('error', 'code') diff --git a/app/services/integrations/aggregator/companies/base_service.rb b/app/services/integrations/aggregator/companies/base_service.rb new file mode 100644 index 00000000000..46a3a249184 --- /dev/null +++ b/app/services/integrations/aggregator/companies/base_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Companies + class BaseService < Integrations::Aggregator::Contacts::BaseService + def action_path + "v1/#{provider}/companies" + end + + private + + def process_hash_result(body) + contact = body['succeededCompanies']&.first + contact_id = contact&.dig('id') + email = contact&.dig('email') + + if contact_id + result.contact_id = contact_id + result.email = email if email.present? + else + message = if body.key?('failedCompanies') + body['failedCompanies'].first['validation_errors'].map { |error| error['Message'] }.join(". ") + else + body.dig('error', 'payload', 'message') + end + + code = 'Validation error' + + deliver_error_webhook(customer:, code:, message:) + end + end + end + end + end +end diff --git a/app/services/integrations/aggregator/companies/create_service.rb b/app/services/integrations/aggregator/companies/create_service.rb new file mode 100644 index 00000000000..11a0978b9da --- /dev/null +++ b/app/services/integrations/aggregator/companies/create_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Companies + class CreateService < BaseService + def initialize(integration:, customer:, subsidiary_id:) + @customer = customer + @subsidiary_id = subsidiary_id + + super(integration:) + end + + def call + response = http_client.post_with_response(params, headers) + body = JSON.parse(response.body) + + if body.is_a?(Hash) + process_hash_result(body) + else + process_string_result(body) + end + + return result unless result.contact_id + + deliver_success_webhook(customer:, webhook_code:) + + result + rescue LagoHttpClient::HttpError => e + raise RequestLimitError(e) if request_limit_error?(e) + + code = code(e) + message = message(e) + + deliver_error_webhook(customer:, code:, message:) + + result.service_failure!(code:, message:) + end + + private + + attr_reader :customer, :subsidiary_id + + def params + Integrations::Aggregator::Companies::Payloads::Factory.new_instance( + integration:, + integration_customer: nil, + customer:, + subsidiary_id: + ).create_body + end + end + end + end +end diff --git a/app/services/integrations/aggregator/companies/payloads/factory.rb b/app/services/integrations/aggregator/companies/payloads/factory.rb new file mode 100644 index 00000000000..e4214d2e1f6 --- /dev/null +++ b/app/services/integrations/aggregator/companies/payloads/factory.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Companies + module Payloads + class Factory + def self.new_instance(integration:, customer:, integration_customer:, subsidiary_id:) + case integration.type.to_s + when 'Integrations::HubspotIntegration' + Integrations::Aggregator::Companies::Payloads::Hubspot.new( + integration:, + customer:, + integration_customer:, + subsidiary_id: + ) + else + raise(NotImplementedError) + end + end + end + end + end + end +end diff --git a/app/services/integrations/aggregator/companies/payloads/hubspot.rb b/app/services/integrations/aggregator/companies/payloads/hubspot.rb new file mode 100644 index 00000000000..5c528d824aa --- /dev/null +++ b/app/services/integrations/aggregator/companies/payloads/hubspot.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Companies + module Payloads + class Hubspot < Integrations::Aggregator::Contacts::Payloads::BasePayload + def create_body + { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => customer_url + } + } + end + + def update_body + { + 'companyId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => customer_url + } + } + } + end + end + end + end + end +end diff --git a/app/services/integrations/aggregator/companies/update_service.rb b/app/services/integrations/aggregator/companies/update_service.rb new file mode 100644 index 00000000000..b2f15d5227e --- /dev/null +++ b/app/services/integrations/aggregator/companies/update_service.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Companies + class UpdateService < BaseService + def initialize(integration:, integration_customer:) + @integration_customer = integration_customer + + raise ArgumentError, 'Integration customer is not a company' if customer.customer_type_individual? + + super(integration:) + end + + def call + response = http_client.put_with_response(params, headers) + body = JSON.parse(response.body) + + if body.is_a?(Hash) + process_hash_result(body) + else + process_string_result(body) + end + + return result unless result.contact_id + + result + rescue LagoHttpClient::HttpError => e + raise RequestLimitError(e) if request_limit_error?(e) + + code = code(e) + message = message(e) + + deliver_error_webhook(customer:, code:, message:) + + result.service_failure!(code:, message:) + end + + delegate :customer, to: :integration_customer + + private + + attr_reader :integration_customer, :subsidiary_id + + def params + Integrations::Aggregator::Companies::Payloads::Factory.new_instance( + integration:, + integration_customer:, + customer:, + subsidiary_id: + ).update_body + end + end + end + end +end diff --git a/app/services/integrations/aggregator/contacts/base_service.rb b/app/services/integrations/aggregator/contacts/base_service.rb index 3ce567862b2..82ee67f3db0 100644 --- a/app/services/integrations/aggregator/contacts/base_service.rb +++ b/app/services/integrations/aggregator/contacts/base_service.rb @@ -18,20 +18,28 @@ def headers } end - def deliver_success_webhook(customer:) + def deliver_success_webhook(customer:, webhook_code:) SendWebhookJob.perform_later( - 'customer.accounting_provider_created', + webhook_code, customer ) end def process_hash_result(body) - contact_id = body['succeededContacts']&.first.try(:[], 'id') + contact = body['succeededContacts']&.first + contact_id = contact&.dig('id') + email = contact&.dig('email') if contact_id result.contact_id = contact_id + result.email = email if email.present? else - message = body['failedContacts'].first['validation_errors'].map { |error| error['Message'] }.join(". ") + message = if body.key?('failedContacts') + body['failedContacts'].first['validation_errors'].map { |error| error['Message'] }.join(". ") + else + body.dig('error', 'payload', 'message') + end + code = 'Validation error' deliver_error_webhook(customer:, code:, message:) @@ -41,6 +49,15 @@ def process_hash_result(body) def process_string_result(body) result.contact_id = body end + + def webhook_code + case provider + when 'hubspot' + 'customer.crm_provider_created' + else + 'customer.accounting_provider_created' + end + end end end end diff --git a/app/services/integrations/aggregator/contacts/create_service.rb b/app/services/integrations/aggregator/contacts/create_service.rb index 021d2b128e6..38ebbf0cd03 100644 --- a/app/services/integrations/aggregator/contacts/create_service.rb +++ b/app/services/integrations/aggregator/contacts/create_service.rb @@ -23,7 +23,7 @@ def call return result unless result.contact_id - deliver_success_webhook(customer:) + deliver_success_webhook(customer:, webhook_code:) result rescue LagoHttpClient::HttpError => e diff --git a/app/services/integrations/aggregator/contacts/payloads/base_payload.rb b/app/services/integrations/aggregator/contacts/payloads/base_payload.rb index 098caeed1e5..0fc798680bd 100644 --- a/app/services/integrations/aggregator/contacts/payloads/base_payload.rb +++ b/app/services/integrations/aggregator/contacts/payloads/base_payload.rb @@ -57,6 +57,12 @@ def email def phone customer.phone.to_s.split(',').first&.strip end + + def customer_url + url = ENV["LAGO_FRONT_URL"].presence || "https://app.getlago.com" + + URI.join(url, "/customer/", customer.id).to_s + end end end end diff --git a/app/services/integrations/aggregator/contacts/payloads/factory.rb b/app/services/integrations/aggregator/contacts/payloads/factory.rb index 32f450f469e..ee699f0e171 100644 --- a/app/services/integrations/aggregator/contacts/payloads/factory.rb +++ b/app/services/integrations/aggregator/contacts/payloads/factory.rb @@ -28,6 +28,13 @@ def self.new_instance(integration:, customer:, integration_customer:, subsidiary integration_customer:, subsidiary_id: ) + when 'Integrations::HubspotIntegration' + Integrations::Aggregator::Contacts::Payloads::Hubspot.new( + integration:, + customer:, + integration_customer:, + subsidiary_id: + ) else raise(NotImplementedError) end diff --git a/app/services/integrations/aggregator/contacts/payloads/hubspot.rb b/app/services/integrations/aggregator/contacts/payloads/hubspot.rb new file mode 100644 index 00000000000..3a648633545 --- /dev/null +++ b/app/services/integrations/aggregator/contacts/payloads/hubspot.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + module Contacts + module Payloads + class Hubspot < BasePayload + def create_body + { + 'properties' => { + 'email' => customer.email, + 'firstname' => customer.firstname, + 'lastname' => customer.lastname, + 'phone' => customer.phone, + 'company' => customer.legal_name, + 'website' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_customer_link' => customer_url + } + } + end + + def update_body + { + 'contactId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'email' => customer.email, + 'firstname' => customer.firstname, + 'lastname' => customer.lastname, + 'phone' => customer.phone, + 'company' => customer.legal_name, + 'website' => customer.url + } + } + } + end + end + end + end + end +end diff --git a/app/services/integrations/aggregator/contacts/payloads/netsuite.rb b/app/services/integrations/aggregator/contacts/payloads/netsuite.rb index d294040f9a2..444f242bdcf 100644 --- a/app/services/integrations/aggregator/contacts/payloads/netsuite.rb +++ b/app/services/integrations/aggregator/contacts/payloads/netsuite.rb @@ -119,12 +119,6 @@ def lines ] end end - - def customer_url - url = ENV["LAGO_FRONT_URL"].presence || "https://app.getlago.com" - - URI.join(url, "/customer/", customer.id).to_s - end end end end diff --git a/app/services/integrations/aggregator/contacts/update_service.rb b/app/services/integrations/aggregator/contacts/update_service.rb index d9a5b6ec0e5..69002ada5ce 100644 --- a/app/services/integrations/aggregator/contacts/update_service.rb +++ b/app/services/integrations/aggregator/contacts/update_service.rb @@ -20,10 +20,6 @@ def call process_string_result(body) end - return result unless result.contact_id - - deliver_success_webhook(customer:) - result rescue LagoHttpClient::HttpError => e raise RequestLimitError(e) if request_limit_error?(e) @@ -36,12 +32,12 @@ def call result.service_failure!(code:, message:) end + delegate :customer, to: :integration_customer + private attr_reader :integration_customer, :subsidiary_id - delegate :customer, to: :integration_customer - def params Integrations::Aggregator::Contacts::Payloads::Factory.new_instance( integration:, diff --git a/app/services/integrations/aggregator/custom_object_service.rb b/app/services/integrations/aggregator/custom_object_service.rb new file mode 100644 index 00000000000..d6d5d3cb1f1 --- /dev/null +++ b/app/services/integrations/aggregator/custom_object_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Integrations + module Aggregator + class CustomObjectService < BaseService + def initialize(integration:, name:) + @name = name + super(integration:) + end + + def action_path + "v1/#{provider}/custom-object" + end + + def call + response = http_client.get(headers:, body:) + + result.custom_object = OpenStruct.new(response) + result + rescue LagoHttpClient::HttpError => e + result.service_failure!(code: e.error_code, message: e.message) + end + + private + + attr_reader :name + + def headers + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{secret_key}", + 'Provider-Config-Key' => provider_key + } + end + + def body + { + 'name' => name + } + end + end + end +end diff --git a/app/services/integrations/aggregator/send_private_app_token_service.rb b/app/services/integrations/aggregator/send_private_app_token_service.rb deleted file mode 100644 index 659390ec348..00000000000 --- a/app/services/integrations/aggregator/send_private_app_token_service.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Integrations - module Aggregator - class SendPrivateAppTokenService < BaseService - def action_path - "connection/#{integration.connection_id}/metadata" - end - - def call - return unless integration.type == 'Integrations::HubspotIntegration' - return unless integration.private_app_token - - payload = { - privateAppToken: integration.private_app_token - } - - response = http_client.post_with_response(payload, headers) - result.response = response - - result - end - - private - - def headers - { - 'Provider-Config-Key' => 'hubspot', - 'Authorization' => "Bearer #{secret_key}" - } - end - end - end -end diff --git a/app/services/integrations/hubspot/companies/deploy_properties_service.rb b/app/services/integrations/hubspot/companies/deploy_properties_service.rb index 126ce4c78df..579ce9a2274 100644 --- a/app/services/integrations/hubspot/companies/deploy_properties_service.rb +++ b/app/services/integrations/hubspot/companies/deploy_properties_service.rb @@ -25,6 +25,7 @@ def call rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private @@ -79,6 +80,15 @@ def payload fieldType: "text", searchableInGlobalSearch: true, formField: true + }, + { + groupName: "companyinformation", + name: "lago_customer_link", + label: "Lago Customer Link", + type: "string", + fieldType: "text", + searchableInGlobalSearch: true, + formField: true } ] }.freeze diff --git a/app/services/integrations/hubspot/contacts/deploy_properties_service.rb b/app/services/integrations/hubspot/contacts/deploy_properties_service.rb index f8eaac96d86..c295969f013 100644 --- a/app/services/integrations/hubspot/contacts/deploy_properties_service.rb +++ b/app/services/integrations/hubspot/contacts/deploy_properties_service.rb @@ -25,6 +25,7 @@ def call rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private @@ -71,6 +72,15 @@ def payload fieldType: "text", searchableInGlobalSearch: true, formField: true + }, + { + groupName: "contactinformation", + name: "lago_customer_link", + label: "Lago Customer Link", + type: "string", + fieldType: "text", + searchableInGlobalSearch: true, + formField: true } ] }.freeze diff --git a/app/services/integrations/hubspot/create_service.rb b/app/services/integrations/hubspot/create_service.rb index 1ed0b8326f1..8b847e1d3e1 100644 --- a/app/services/integrations/hubspot/create_service.rb +++ b/app/services/integrations/hubspot/create_service.rb @@ -22,7 +22,6 @@ def call name: params[:name], code: params[:code], connection_id: params[:connection_id], - private_app_token: params[:private_app_token], default_targeted_object: params[:default_targeted_object], sync_invoices: ActiveModel::Type::Boolean.new.cast(params[:sync_invoices]), sync_subscriptions: ActiveModel::Type::Boolean.new.cast(params[:sync_subscriptions]) @@ -31,8 +30,8 @@ def call integration.save! if integration.type == 'Integrations::HubspotIntegration' - Integrations::Aggregator::SendPrivateAppTokenJob.perform_later(integration:) Integrations::Aggregator::SyncCustomObjectsAndPropertiesJob.perform_later(integration:) + Integrations::Hubspot::SavePortalIdJob.perform_later(integration:) end result.integration = integration diff --git a/app/services/integrations/hubspot/invoices/deploy_object_service.rb b/app/services/integrations/hubspot/invoices/deploy_object_service.rb index 5da9dfca39c..9e500e8175b 100644 --- a/app/services/integrations/hubspot/invoices/deploy_object_service.rb +++ b/app/services/integrations/hubspot/invoices/deploy_object_service.rb @@ -13,22 +13,35 @@ def action_path def call return unless integration.type == 'Integrations::HubspotIntegration' return result if integration.invoices_properties_version == VERSION + + custom_object_result = Integrations::Aggregator::CustomObjectService.call(integration:, name: 'LagoInvoices') + if custom_object_result.success? + save_object_type_id(custom_object_result.custom_object&.objectTypeId) + return result + end + response = nil ActiveRecord::Base.transaction do response = http_client.post_with_response(payload, headers) - integration.settings = integration.reload.settings - integration.invoices_properties_version = VERSION - integration.save! + save_object_type_id(response['objectTypeId']) end result.response = response result rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private + def save_object_type_id(object_type_id) + integration.settings = integration.reload.settings + integration.invoices_object_type_id = object_type_id + integration.invoices_properties_version = VERSION + integration.save! + end + def headers { 'Provider-Config-Key' => 'hubspot', diff --git a/app/services/integrations/hubspot/invoices/deploy_properties_service.rb b/app/services/integrations/hubspot/invoices/deploy_properties_service.rb index d4635fe80d4..bfceaf9d9ef 100644 --- a/app/services/integrations/hubspot/invoices/deploy_properties_service.rb +++ b/app/services/integrations/hubspot/invoices/deploy_properties_service.rb @@ -25,6 +25,7 @@ def call rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private diff --git a/app/services/integrations/hubspot/save_portal_id_service.rb b/app/services/integrations/hubspot/save_portal_id_service.rb new file mode 100644 index 00000000000..ad21ddfe540 --- /dev/null +++ b/app/services/integrations/hubspot/save_portal_id_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Integrations + module Hubspot + class SavePortalIdService < BaseService + def initialize(integration:) + @integration = integration + super + end + + def call + return result unless integration.type == 'Integrations::HubspotIntegration' + return result if integration.portal_id.present? + + account_information_result = Integrations::Aggregator::AccountInformationService.call(integration:) + + integration.update!(portal_id: account_information_result.account_information.id) + + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + private + + attr_reader :integration + end + end +end diff --git a/app/services/integrations/hubspot/subscriptions/deploy_object_service.rb b/app/services/integrations/hubspot/subscriptions/deploy_object_service.rb index 81cf0c8d30d..52f548e70a8 100644 --- a/app/services/integrations/hubspot/subscriptions/deploy_object_service.rb +++ b/app/services/integrations/hubspot/subscriptions/deploy_object_service.rb @@ -17,6 +17,7 @@ def call ActiveRecord::Base.transaction do response = http_client.post_with_response(payload, headers) integration.settings = integration.reload.settings + integration.subscriptions_object_type_id = response['objectTypeId'] integration.subscriptions_properties_version = VERSION integration.save! end @@ -25,6 +26,7 @@ def call rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private diff --git a/app/services/integrations/hubspot/subscriptions/deploy_properties_service.rb b/app/services/integrations/hubspot/subscriptions/deploy_properties_service.rb index 7fa87272e91..800be3ed0e8 100644 --- a/app/services/integrations/hubspot/subscriptions/deploy_properties_service.rb +++ b/app/services/integrations/hubspot/subscriptions/deploy_properties_service.rb @@ -25,6 +25,7 @@ def call rescue LagoHttpClient::HttpError => e message = message(e) deliver_integration_error_webhook(integration:, code: 'integration_error', message:) + result end private diff --git a/app/services/integrations/hubspot/update_service.rb b/app/services/integrations/hubspot/update_service.rb index a661ed68406..2470281b223 100644 --- a/app/services/integrations/hubspot/update_service.rb +++ b/app/services/integrations/hubspot/update_service.rb @@ -17,21 +17,14 @@ def call return result.not_allowed_failure!(code: 'premium_integration_missing') end - old_private_app_token = integration.private_app_token - integration.name = params[:name] if params.key?(:name) integration.code = params[:code] if params.key?(:code) - integration.private_app_token = params[:private_app_token] if params.key?(:private_app_token) integration.default_targeted_object = params[:default_targeted_object] if params.key?(:default_targeted_object) integration.sync_invoices = params[:sync_invoices] if params.key?(:sync_invoices) integration.sync_subscriptions = params[:sync_subscriptions] if params.key?(:sync_subscriptions) integration.save! - if integration.type == 'Integrations::HubspotIntegration' && integration.private_app_token != old_private_app_token - Integrations::Aggregator::SendPrivateAppTokenJob.perform_later(integration:) - end - result.integration = integration result rescue ActiveRecord::RecordInvalid => e diff --git a/app/services/webhooks/integrations/accounting_customer_created_service.rb b/app/services/webhooks/integrations/accounting_customer_created_service.rb new file mode 100644 index 00000000000..07299f4cd48 --- /dev/null +++ b/app/services/webhooks/integrations/accounting_customer_created_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Webhooks + module Integrations + class AccountingCustomerCreatedService < CustomerCreatedService + private + + def webhook_type + 'customer.accounting_provider_created' + end + end + end +end diff --git a/app/services/webhooks/integrations/accounting_customer_error_service.rb b/app/services/webhooks/integrations/accounting_customer_error_service.rb new file mode 100644 index 00000000000..567525c8800 --- /dev/null +++ b/app/services/webhooks/integrations/accounting_customer_error_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Webhooks + module Integrations + class AccountingCustomerErrorService < CustomerErrorService + private + + def webhook_type + 'customer.accounting_provider_error' + end + + def object_type + 'accounting_provider_customer_error' + end + end + end +end diff --git a/app/services/webhooks/integrations/crm_customer_created_service.rb b/app/services/webhooks/integrations/crm_customer_created_service.rb new file mode 100644 index 00000000000..689a3559dde --- /dev/null +++ b/app/services/webhooks/integrations/crm_customer_created_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Webhooks + module Integrations + class CrmCustomerCreatedService < CustomerCreatedService + private + + def webhook_type + 'customer.crm_provider_created' + end + end + end +end diff --git a/app/services/webhooks/integrations/crm_customer_error_service.rb b/app/services/webhooks/integrations/crm_customer_error_service.rb new file mode 100644 index 00000000000..676982fc90b --- /dev/null +++ b/app/services/webhooks/integrations/crm_customer_error_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Webhooks + module Integrations + class CrmCustomerErrorService < CustomerErrorService + private + + def webhook_type + 'customer.crm_provider_error' + end + + def object_type + 'crm_provider_customer_error' + end + end + end +end diff --git a/app/services/webhooks/integrations/customer_created_service.rb b/app/services/webhooks/integrations/customer_created_service.rb index d1af96a0d96..d0951faede9 100644 --- a/app/services/webhooks/integrations/customer_created_service.rb +++ b/app/services/webhooks/integrations/customer_created_service.rb @@ -17,10 +17,6 @@ def object_serializer ) end - def webhook_type - 'customer.accounting_provider_created' - end - def object_type 'customer' end diff --git a/app/services/webhooks/integrations/customer_error_service.rb b/app/services/webhooks/integrations/customer_error_service.rb index f8b07f8baca..22e5a03bb79 100644 --- a/app/services/webhooks/integrations/customer_error_service.rb +++ b/app/services/webhooks/integrations/customer_error_service.rb @@ -18,14 +18,6 @@ def object_serializer provider_code: options[:provider_code] ) end - - def webhook_type - 'customer.accounting_provider_error' - end - - def object_type - 'accounting_provider_customer_error' - end end end end diff --git a/lib/lago_http_client/lago_http_client/client.rb b/lib/lago_http_client/lago_http_client/client.rb index b58f07a3be4..eddeec7cc26 100644 --- a/lib/lago_http_client/lago_http_client/client.rb +++ b/lib/lago_http_client/lago_http_client/client.rb @@ -91,9 +91,10 @@ def post_url_encoded(params, headers) JSON.parse(response.body.presence || '{}') end - def get(headers: {}, params: nil) + def get(headers: {}, params: nil, body: nil) path = params ? "#{uri.path}?#{URI.encode_www_form(params)}" : uri.path req = Net::HTTP::Get.new(path) + req.body = URI.encode_www_form(body) if body.present? headers.keys.each do |key| req[key] = headers[key] diff --git a/schema.graphql b/schema.graphql index 34edbc1a76f..274f3087b2d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2031,7 +2031,6 @@ input CreateHubspotIntegrationInput { connectionId: String! defaultTargetedObject: TargetedObjectsEnum! name: String! - privateAppToken: String! syncInvoices: Boolean syncSubscriptions: Boolean } @@ -4145,7 +4144,6 @@ type HubspotIntegration { defaultTargetedObject: TargetedObjectsEnum! id: ID! name: String! - privateAppToken: String! syncInvoices: Boolean syncSubscriptions: Boolean } @@ -7903,7 +7901,6 @@ input UpdateHubspotIntegrationInput { defaultTargetedObject: TargetedObjectsEnum id: ID name: String - privateAppToken: String syncInvoices: Boolean syncSubscriptions: Boolean } diff --git a/schema.json b/schema.json index 9c4985e1117..03527ea2d36 100644 --- a/schema.json +++ b/schema.json @@ -8106,22 +8106,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "privateAppToken", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "syncInvoices", "description": null, @@ -19915,24 +19899,6 @@ ] }, - { - "name": "privateAppToken", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null, - "args": [ - - ] - }, { "name": "syncInvoices", "description": null, @@ -38960,18 +38926,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "privateAppToken", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "syncInvoices", "description": null, diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index 6f01830cbbc..13d3833ed0e 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -74,7 +74,7 @@ end secrets do - {connection_id: SecureRandom.uuid, private_app_token: SecureRandom.uuid}.to_json + {connection_id: SecureRandom.uuid}.to_json end end end diff --git a/spec/fixtures/integration_aggregator/account_information_response.json b/spec/fixtures/integration_aggregator/account_information_response.json new file mode 100644 index 00000000000..3e2fc62e838 --- /dev/null +++ b/spec/fixtures/integration_aggregator/account_information_response.json @@ -0,0 +1,11 @@ +{ + "id": "1234567890", + "type": "STANDARD", + "timeZone": "US/Eastern", + "companyCurrency": "USD", + "additionalCurrencies": [], + "utcOffset": "-04:00", + "utcOffsetMilliseconds": -14400000, + "uiDomain": "app.hubspot.com", + "dataHostingLocation": "na1" +} diff --git a/spec/fixtures/integration_aggregator/companies/failure_hash_response.json b/spec/fixtures/integration_aggregator/companies/failure_hash_response.json new file mode 100644 index 00000000000..e7b5e80b9a6 --- /dev/null +++ b/spec/fixtures/integration_aggregator/companies/failure_hash_response.json @@ -0,0 +1,20 @@ +{ + "succeededCompanies": [], + "failedCompanies": [ + { + "id": "2e50c200-9a54-4a66-b241-1e75fb87373f", + "name": "Test", + "email": "s@", + "city": "z", + "zip": "73993", + "country": "US", + "state": "NYC", + "phone": "+33818282828", + "validation_errors": [ + { + "Message": "Email address must be valid." + } + ] + } + ] +} diff --git a/spec/fixtures/integration_aggregator/companies/success_hash_response.json b/spec/fixtures/integration_aggregator/companies/success_hash_response.json new file mode 100644 index 00000000000..2c07a1ee010 --- /dev/null +++ b/spec/fixtures/integration_aggregator/companies/success_hash_response.json @@ -0,0 +1,16 @@ +{ + "succeededCompanies": [ + { + "id": "2e50c200-9a54-4a66-b241-1e75fb87373f", + "name": "Test", + "email": "roger@rogers.com", + "lago_billing_email": "roger@rogers.com", + "city": "NYC", + "zip": "73993", + "country": "US", + "state": "NYC", + "phone": "+33818282828" + } + ], + "failedCompanies": [] +} diff --git a/spec/fixtures/integration_aggregator/companies/success_string_response.json b/spec/fixtures/integration_aggregator/companies/success_string_response.json new file mode 100644 index 00000000000..f27b76c59aa --- /dev/null +++ b/spec/fixtures/integration_aggregator/companies/success_string_response.json @@ -0,0 +1 @@ +"1" diff --git a/spec/fixtures/integration_aggregator/custom_object_response.json b/spec/fixtures/integration_aggregator/custom_object_response.json new file mode 100644 index 00000000000..375c25dd68f --- /dev/null +++ b/spec/fixtures/integration_aggregator/custom_object_response.json @@ -0,0 +1,1310 @@ +{ + "labels": { + "singular": "LagoInvoice", + "plural": "LagoInvoices" + }, + "requiredProperties": [ + "lago_invoice_id", + "lago_invoice_number" + ], + "searchableProperties": [ + "lago_invoice_id", + "lago_invoice_number" + ], + "primaryDisplayProperty": "lago_invoice_number", + "secondaryDisplayProperties": [ + "lago_invoice_status", + "lago_invoice_id" + ], + "description": "Invoices issued by Lago billing engine", + "archived": false, + "restorable": true, + "metaType": "PORTAL_SPECIFIC", + "id": "35482707", + "fullyQualifiedName": "p46788684_LagoInvoices", + "createdAt": "2024-10-10T16:44:12.791Z", + "updatedAt": "2024-10-10T16:44:14.736Z", + "createdByUserId": 68744312, + "updatedByUserId": 68744312, + "objectTypeId": "2-35482707", + "properties": [ + { + "name": "hs_all_accessible_team_ids", + "label": "All teams", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The team IDs, including the team hierarchy, of all default and custom owner properties for this record.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_all_assigned_business_unit_ids", + "label": "Business units", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The business units this record is assigned to.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_all_owner_ids", + "label": "All owner IDs", + "type": "enumeration", + "fieldType": "checkbox", + "description": "Values of all default and custom owner properties for this record.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_all_team_ids", + "label": "All team IDs", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The team IDs of all default and custom owner properties for this record.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_created_by_user_id", + "label": "Created by user ID", + "type": "number", + "fieldType": "number", + "description": "The user who created this record. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_createdate", + "label": "Object create date/time", + "type": "datetime", + "fieldType": "date", + "description": "The date and time at which this object was created. This value is automatically set by HubSpot and may not be modified.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_lastmodifieddate", + "label": "Object last modified date/time", + "type": "datetime", + "fieldType": "date", + "description": "Most recent timestamp of any property update for this object. This includes HubSpot internal properties, which can be visible or hidden. This property is updated automatically.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_merged_object_ids", + "label": "Merged record IDs", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The list of record IDs that have been merged into this record. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_id", + "label": "Record ID", + "type": "number", + "fieldType": "number", + "description": "The unique ID for this record. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source", + "label": "Record creation source", + "type": "string", + "fieldType": "text", + "description": "Raw internal PropertySource present in the RequestMeta when this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_detail_1", + "label": "Record source detail 1", + "type": "string", + "fieldType": "text", + "description": "First level of detail on how this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_detail_2", + "label": "Record source detail 2", + "type": "string", + "fieldType": "text", + "description": "Second level of detail on how this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_detail_3", + "label": "Record source detail 3", + "type": "string", + "fieldType": "text", + "description": "Third level of detail on how this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_id", + "label": "Record creation source ID", + "type": "string", + "fieldType": "text", + "description": "Raw internal sourceId present in the RequestMeta when this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_label", + "label": "Record source", + "type": "enumeration", + "fieldType": "select", + "description": "How this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_object_source_user_id", + "label": "Record creation source user ID", + "type": "number", + "fieldType": "number", + "description": "Raw internal userId present in the RequestMeta when this record was created.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:13.352Z", + "createdAt": "2024-10-10T16:44:13.352Z", + "name": "hs_pinned_engagement_id", + "label": "Pinned Engagement ID", + "type": "number", + "fieldType": "number", + "description": "The object ID of the current pinned engagement. This will only be shown in the app if there is already an association to the engagement.", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_read_only", + "label": "Read only object", + "type": "bool", + "fieldType": "booleancheckbox", + "description": "Determines whether a record can be edited by a user.", + "groupName": "lagoinvoices_information", + "options": [ + { + "label": "True", + "value": "true", + "displayOrder": 0, + "hidden": false + }, + { + "label": "False", + "value": "false", + "displayOrder": 1, + "hidden": false + } + ], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_shared_team_ids", + "label": "Shared teams", + "type": "enumeration", + "fieldType": "checkbox", + "description": "Additional teams whose users can access the record based on their permissions. This can be set manually or through Workflows or APIs.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_shared_user_ids", + "label": "Shared users", + "type": "enumeration", + "fieldType": "checkbox", + "description": "Additional users that can access the record based on their permissions. This can be set manually or through Workflows and APIs.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_unique_creation_key", + "label": "Unique creation key", + "type": "string", + "fieldType": "text", + "description": "Unique property used for idempotent creates", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": true, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_updated_by_user_id", + "label": "Updated by user ID", + "type": "number", + "fieldType": "number", + "description": "The user who last updated this record. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_user_ids_of_all_notification_followers", + "label": "User IDs of all notification followers", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The user IDs of all users that have clicked follow within the object to opt-in to getting follow notifications", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_user_ids_of_all_notification_unfollowers", + "label": "User IDs of all notification unfollowers", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The user IDs of all object owners that have clicked unfollow within the object to opt-out of getting follow notifications", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_user_ids_of_all_owners", + "label": "User IDs of all owners", + "type": "enumeration", + "fieldType": "checkbox", + "description": "The user IDs of all owners of this record.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hs_was_imported", + "label": "Performed in an import", + "type": "bool", + "fieldType": "booleancheckbox", + "description": "Object is part of an import", + "groupName": "lagoinvoices_information", + "options": [ + { + "label": "True", + "value": "true", + "displayOrder": 0, + "hidden": false + }, + { + "label": "False", + "value": "false", + "displayOrder": 1, + "hidden": false + } + ], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": true, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hubspot_owner_assigneddate", + "label": "Owner assigned date", + "type": "datetime", + "fieldType": "date", + "description": "The most recent timestamp of when an owner was assigned to this record. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hubspot_owner_id", + "label": "Owner", + "type": "enumeration", + "fieldType": "select", + "description": "The owner of the object.", + "groupName": "lagoinvoices_information", + "options": [], + "referencedObjectType": "OWNER", + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "name": "hubspot_team_id", + "label": "Owner's main team", + "type": "enumeration", + "fieldType": "select", + "description": "The main team of the record owner. This value is set automatically by HubSpot.", + "groupName": "lagoinvoices_information", + "options": [], + "displayOrder": -1, + "calculated": false, + "externalOptions": true, + "hasUniqueValue": false, + "hidden": false, + "hubspotDefined": true, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": true, + "readOnlyValue": true + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_currency", + "label": "Lago Invoice Currency", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_file_url", + "label": "Lago Invoice File URL", + "type": "string", + "fieldType": "file", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_id", + "label": "Lago Invoice Id", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": true, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_issuing_date", + "label": "Lago Invoice Issuing Date", + "type": "date", + "fieldType": "date", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_number", + "label": "Lago Invoice Number", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_payment_due_date", + "label": "Lago Invoice Payment Due Date", + "type": "date", + "fieldType": "date", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_payment_overdue", + "label": "Lago Invoice Payment Overdue", + "type": "bool", + "fieldType": "booleancheckbox", + "description": "", + "groupName": "lagoinvoices", + "options": [ + { + "label": "True", + "value": "true", + "displayOrder": 0, + "hidden": false + }, + { + "label": "False", + "value": "false", + "displayOrder": 1, + "hidden": false + } + ], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_payment_status", + "label": "Lago Invoice Payment Status", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_status", + "label": "Lago Invoice Status", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_subtotal_excluding_taxes", + "label": "Lago Invoice Subtotal Excluding Taxes", + "type": "number", + "fieldType": "number", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_total_amount", + "label": "Lago Invoice Total Amount", + "type": "number", + "fieldType": "number", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + }, + { + "updatedAt": "2024-10-10T16:44:12.996Z", + "createdAt": "2024-10-10T16:44:12.996Z", + "name": "lago_invoice_type", + "label": "Lago Invoice Type", + "type": "string", + "fieldType": "text", + "description": "", + "groupName": "lagoinvoices_information", + "options": [], + "createdUserId": "68744312", + "updatedUserId": "68744312", + "displayOrder": -1, + "calculated": false, + "externalOptions": false, + "archived": false, + "hasUniqueValue": false, + "hidden": false, + "modificationMetadata": { + "archivable": true, + "readOnlyDefinition": false, + "readOnlyValue": false + }, + "formField": false, + "dataSensitivity": "non_sensitive" + } + ], + "associations": [ + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-116", + "name": "lagoinvoices_to_postal_mail", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "615", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-116", + "toObjectTypeId": "2-35482707", + "name": "lagoinvoices_to_postal_mail", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "616", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-18", + "name": "communication_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "619", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-18", + "toObjectTypeId": "2-35482707", + "name": "communication_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "620", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-27", + "name": "lagoinvoices_to_task", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "611", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-27", + "toObjectTypeId": "2-35482707", + "name": "lagoinvoices_to_task", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "612", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-2", + "name": "company_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 50000, + "maxFromObjectIds": 50000, + "id": "627", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-2", + "toObjectTypeId": "2-35482707", + "name": "company_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 50000, + "maxFromObjectIds": 50000, + "id": "628", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-47", + "name": "lagoinvoices_to_meeting_event", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "617", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-47", + "toObjectTypeId": "2-35482707", + "name": "lagoinvoices_to_meeting_event", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "618", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-46", + "name": "lagoinvoices_to_note", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "623", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-46", + "toObjectTypeId": "2-35482707", + "name": "lagoinvoices_to_note", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "624", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-48", + "name": "call_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "621", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-48", + "toObjectTypeId": "2-35482707", + "name": "call_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "622", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-51", + "name": "conversation_session_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "609", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-51", + "toObjectTypeId": "2-35482707", + "name": "conversation_session_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "610", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-1", + "name": "contact_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 50000, + "maxFromObjectIds": 50000, + "id": "625", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-1", + "toObjectTypeId": "2-35482707", + "name": "contact_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 50000, + "maxFromObjectIds": 50000, + "id": "626", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "2-35482707", + "toObjectTypeId": "0-49", + "name": "email_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "613", + "createdAt": null, + "updatedAt": null + }, + { + "fromObjectTypeId": "0-49", + "toObjectTypeId": "2-35482707", + "name": "email_to_lagoinvoices", + "cardinality": "ONE_TO_MANY", + "inverseCardinality": "ONE_TO_MANY", + "hasUserEnforcedMaxToObjectIds": false, + "hasUserEnforcedMaxFromObjectIds": false, + "maxToObjectIds": 10000, + "maxFromObjectIds": 10000, + "id": "614", + "createdAt": null, + "updatedAt": null + } + ], + "name": "LagoInvoices" +} diff --git a/spec/graphql/mutations/integrations/hubspot/create_spec.rb b/spec/graphql/mutations/integrations/hubspot/create_spec.rb index aed1d2906fd..23bf15ed90e 100644 --- a/spec/graphql/mutations/integrations/hubspot/create_spec.rb +++ b/spec/graphql/mutations/integrations/hubspot/create_spec.rb @@ -18,7 +18,6 @@ name, connectionId, defaultTargetedObject, - privateAppToken, syncInvoices, syncSubscriptions } @@ -45,8 +44,7 @@ code:, name:, connectionId: 'this-is-random-uuid', - defaultTargetedObject: 'companies', - privateAppToken: 'some-private-app-token' + defaultTargetedObject: 'companies' } } ) diff --git a/spec/graphql/mutations/integrations/hubspot/update_spec.rb b/spec/graphql/mutations/integrations/hubspot/update_spec.rb index 9cfabf01315..ed518d4a852 100644 --- a/spec/graphql/mutations/integrations/hubspot/update_spec.rb +++ b/spec/graphql/mutations/integrations/hubspot/update_spec.rb @@ -19,7 +19,6 @@ name, connectionId, defaultTargetedObject, - privateAppToken, syncInvoices, syncSubscriptions } diff --git a/spec/graphql/types/integrations/hubspot/create_input_spec.rb b/spec/graphql/types/integrations/hubspot/create_input_spec.rb index ec245a88088..2417b393a41 100644 --- a/spec/graphql/types/integrations/hubspot/create_input_spec.rb +++ b/spec/graphql/types/integrations/hubspot/create_input_spec.rb @@ -9,7 +9,6 @@ it { is_expected.to accept_argument(:name).of_type('String!') } it { is_expected.to accept_argument(:connection_id).of_type('String!') } it { is_expected.to accept_argument(:default_targeted_object).of_type('TargetedObjectsEnum!') } - it { is_expected.to accept_argument(:private_app_token).of_type('String!') } it { is_expected.to accept_argument(:sync_invoices).of_type('Boolean') } it { is_expected.to accept_argument(:sync_subscriptions).of_type('Boolean') } end diff --git a/spec/graphql/types/integrations/hubspot/update_input_spec.rb b/spec/graphql/types/integrations/hubspot/update_input_spec.rb index 59b83a2d9b7..6ed27d469cb 100644 --- a/spec/graphql/types/integrations/hubspot/update_input_spec.rb +++ b/spec/graphql/types/integrations/hubspot/update_input_spec.rb @@ -10,7 +10,6 @@ it { is_expected.to accept_argument(:name).of_type('String') } it { is_expected.to accept_argument(:connection_id).of_type('String') } it { is_expected.to accept_argument(:default_targeted_object).of_type('TargetedObjectsEnum') } - it { is_expected.to accept_argument(:private_app_token).of_type('String') } it { is_expected.to accept_argument(:sync_invoices).of_type('Boolean') } it { is_expected.to accept_argument(:sync_subscriptions).of_type('Boolean') } end diff --git a/spec/graphql/types/integrations/hubspot_spec.rb b/spec/graphql/types/integrations/hubspot_spec.rb index ed96f236384..20c2122e3cb 100644 --- a/spec/graphql/types/integrations/hubspot_spec.rb +++ b/spec/graphql/types/integrations/hubspot_spec.rb @@ -11,7 +11,6 @@ it { is_expected.to have_field(:connection_id).of_type('ID!') } it { is_expected.to have_field(:default_targeted_object).of_type('TargetedObjectsEnum!') } it { is_expected.to have_field(:name).of_type('String!') } - it { is_expected.to have_field(:private_app_token).of_type('String!') } it { is_expected.to have_field(:sync_invoices).of_type('Boolean') } it { is_expected.to have_field(:sync_subscriptions).of_type('Boolean') } diff --git a/spec/jobs/integrations/aggregator/send_private_app_token_job_spec.rb b/spec/jobs/integrations/aggregator/send_private_app_token_job_spec.rb deleted file mode 100644 index f0f195ccf39..00000000000 --- a/spec/jobs/integrations/aggregator/send_private_app_token_job_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Integrations::Aggregator::SendPrivateAppTokenJob, type: :job do - describe '#perform' do - subject(:send_token_job) { described_class } - - let(:send_token_service) { instance_double(Integrations::Aggregator::SendPrivateAppTokenService) } - let(:integration) { create(:hubspot_integration) } - let(:result) { BaseService::Result.new } - - before do - allow(Integrations::Aggregator::SendPrivateAppTokenService).to receive(:new).and_return(send_token_service) - allow(send_token_service).to receive(:call).and_return(result) - end - - it 'sends the private app token the nango' do - described_class.perform_now(integration:) - - expect(Integrations::Aggregator::SendPrivateAppTokenService).to have_received(:new) - expect(send_token_service).to have_received(:call) - end - end -end diff --git a/spec/jobs/integrations/hubspot/save_portal_id_job_spec.rb b/spec/jobs/integrations/hubspot/save_portal_id_job_spec.rb new file mode 100644 index 00000000000..36a8c6698b3 --- /dev/null +++ b/spec/jobs/integrations/hubspot/save_portal_id_job_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Hubspot::SavePortalIdJob, type: :job do + describe '#perform' do + subject(:job) { described_class } + + let(:service) { instance_double(Integrations::Hubspot::SavePortalIdService) } + let(:integration) { create(:hubspot_integration) } + let(:result) { BaseService::Result.new } + + before do + allow(Integrations::Hubspot::SavePortalIdService).to receive(:new).and_return(service) + allow(service).to receive(:call).and_return(result) + end + + it 'saves portal id to the integration' do + described_class.perform_now(integration:) + + expect(Integrations::Hubspot::SavePortalIdService).to have_received(:new) + expect(service).to have_received(:call) + end + end +end diff --git a/spec/jobs/send_webhook_job_spec.rb b/spec/jobs/send_webhook_job_spec.rb index e14d1c0c730..46fa3460e6d 100644 --- a/spec/jobs/send_webhook_job_spec.rb +++ b/spec/jobs/send_webhook_job_spec.rb @@ -226,6 +226,50 @@ end end + context 'when webhook_type is customer.accounting_provider_created' do + let(:webhook_service) { instance_double(Webhooks::Integrations::AccountingCustomerCreatedService) } + let(:customer) { create(:customer) } + + before do + allow(Webhooks::Integrations::AccountingCustomerCreatedService).to receive(:new) + .with(object: customer, options: {}) + .and_return(webhook_service) + allow(webhook_service).to receive(:call) + end + + it 'calls the webhook event service' do + send_webhook_job.perform_now( + 'customer.accounting_provider_created', + customer + ) + + expect(Webhooks::Integrations::AccountingCustomerCreatedService).to have_received(:new) + expect(webhook_service).to have_received(:call) + end + end + + context 'when webhook_type is customer.crm_provider_created' do + let(:webhook_service) { instance_double(Webhooks::Integrations::CrmCustomerCreatedService) } + let(:customer) { create(:customer) } + + before do + allow(Webhooks::Integrations::CrmCustomerCreatedService).to receive(:new) + .with(object: customer, options: {}) + .and_return(webhook_service) + allow(webhook_service).to receive(:call) + end + + it 'calls the webhook event service' do + send_webhook_job.perform_now( + 'customer.crm_provider_created', + customer + ) + + expect(Webhooks::Integrations::CrmCustomerCreatedService).to have_received(:new) + expect(webhook_service).to have_received(:call) + end + end + context 'when webhook_type is customer.checkout_url_generated' do let(:webhook_service) { instance_double(Webhooks::PaymentProviders::CustomerCheckoutService) } let(:customer) { create(:customer) } diff --git a/spec/models/integrations/hubspot_integration_spec.rb b/spec/models/integrations/hubspot_integration_spec.rb index 5a7ff471fd9..9f154c1a22e 100644 --- a/spec/models/integrations/hubspot_integration_spec.rb +++ b/spec/models/integrations/hubspot_integration_spec.rb @@ -7,7 +7,6 @@ it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:connection_id) } - it { is_expected.to validate_presence_of(:private_app_token) } it { is_expected.to validate_presence_of(:default_targeted_object) } describe 'validations' do @@ -23,13 +22,6 @@ end end - describe '#private_app_token' do - it 'assigns and retrieve a secret pair' do - hubspot_integration.private_app_token = 'secret_token' - expect(hubspot_integration.private_app_token).to eq('secret_token') - end - end - describe '#default_targeted_object' do it 'assigns and retrieve a setting' do hubspot_integration.default_targeted_object = 'companies' @@ -37,6 +29,13 @@ end end + describe '#portal_id' do + it 'assigns and retrieve a setting' do + hubspot_integration.portal_id = '123456789' + expect(hubspot_integration.portal_id).to eq('123456789') + end + end + describe '#sync_invoices' do it 'assigns and retrieve a setting' do hubspot_integration.sync_invoices = true diff --git a/spec/services/integration_customers/hubspot_service_spec.rb b/spec/services/integration_customers/hubspot_service_spec.rb new file mode 100644 index 00000000000..51feb1fd4e6 --- /dev/null +++ b/spec/services/integration_customers/hubspot_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe IntegrationCustomers::HubspotService, type: :service do + let(:integration) { create(:xero_integration, organization:) } + let(:organization) { membership.organization } + let(:membership) { create(:membership) } + let(:customer) { create(:customer, organization:, customer_type: 'individual') } + + describe '#create' do + subject(:service_call) { described_class.new(integration:, customer:, subsidiary_id: nil, targeted_object: 'contacts').create } + + let(:contact_id) { SecureRandom.uuid } + let(:create_result) do + result = BaseService::Result.new + result.contact_id = contact_id + result.email = customer.email + result + end + let(:aggregator_contacts_create_service) do + instance_double(Integrations::Aggregator::Contacts::CreateService) + end + + before do + allow(Integrations::Aggregator::Contacts::CreateService) + .to receive(:new).and_return(aggregator_contacts_create_service) + + allow(aggregator_contacts_create_service).to receive(:call).and_return(create_result) + end + + it 'returns integration customer' do + result = service_call + + aggregate_failures do + expect(aggregator_contacts_create_service).to have_received(:call) + expect(result).to be_success + expect(result.integration_customer.external_customer_id).to eq(contact_id) + expect(result.integration_customer.integration_id).to eq(integration.id) + expect(result.integration_customer.customer_id).to eq(customer.id) + expect(result.integration_customer.email).to eq(customer.email) + expect(result.integration_customer.type).to eq('IntegrationCustomers::HubspotCustomer') + end + end + + it 'creates integration customer' do + expect { service_call }.to change(IntegrationCustomers::HubspotCustomer, :count).by(1) + end + end +end diff --git a/spec/services/integration_customers/update_service_spec.rb b/spec/services/integration_customers/update_service_spec.rb index 742098ff7a5..f4c0a0c6c8f 100644 --- a/spec/services/integration_customers/update_service_spec.rb +++ b/spec/services/integration_customers/update_service_spec.rb @@ -83,7 +83,6 @@ expect(aggregator_contacts_update_service).to have_received(:call) expect(result).to be_success expect(result.integration_customer).to eq(integration_customer) - expect(result.integration_customer.subsidiary_id).to eq(subsidiary_id) end end end @@ -100,12 +99,6 @@ expect(result.integration_customer).to eq(integration_customer) end end - - it 'updates integration customer' do - result = service_call - - expect(result.integration_customer.subsidiary_id).to eq(subsidiary_id) - end end context 'with anrok customer' do @@ -132,10 +125,10 @@ context 'when customer external id is present' do let(:external_customer_id) { SecureRandom.uuid } - it 'does not calls aggregator update service' do + it 'calls aggregator update service' do service_call - expect(aggregator_contacts_update_service).not_to have_received(:call) + expect(aggregator_contacts_update_service).to have_received(:call) end it 'updates integration customer' do @@ -145,20 +138,6 @@ end end - context 'when subsidiary id is present' do - it 'does not calls aggregator update service' do - service_call - - expect(aggregator_contacts_update_service).not_to have_received(:call) - end - - it 'does not save subsidiary id' do - result = service_call - - expect(result.integration_customer.subsidiary_id).not_to eq(subsidiary_id) - end - end - context 'when customer external id is not present' do let(:external_customer_id) { nil } diff --git a/spec/services/integrations/aggregator/account_information_service_spec.rb b/spec/services/integrations/aggregator/account_information_service_spec.rb new file mode 100644 index 00000000000..e80c1518b97 --- /dev/null +++ b/spec/services/integrations/aggregator/account_information_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::AccountInformationService do + subject(:account_information_service) { described_class.new(integration:) } + + let(:integration) { create(:hubspot_integration) } + + describe '.call' do + let(:lago_client) { instance_double(LagoHttpClient::Client) } + let(:endpoint) { 'https://api.nango.dev/v1/account-information' } + + let(:headers) do + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{ENV["NANGO_SECRET_KEY"]}", + 'Provider-Config-Key' => 'hubspot' + } + end + + let(:aggregator_response) do + path = Rails.root.join('spec/fixtures/integration_aggregator/account_information_response.json') + JSON.parse(File.read(path)) + end + + before do + allow(LagoHttpClient::Client).to receive(:new).with(endpoint).and_return(lago_client) + allow(lago_client).to receive(:get).with(headers:).and_return(aggregator_response) + end + + it 'successfully fetches account information' do + result = account_information_service.call + account_information = result.account_information + + aggregate_failures do + expect(LagoHttpClient::Client).to have_received(:new).with(endpoint) + expect(lago_client).to have_received(:get) + expect(account_information.id).to eq('1234567890') + end + end + end +end diff --git a/spec/services/integrations/aggregator/companies/create_service_spec.rb b/spec/services/integrations/aggregator/companies/create_service_spec.rb new file mode 100644 index 00000000000..d825aea5a71 --- /dev/null +++ b/spec/services/integrations/aggregator/companies/create_service_spec.rb @@ -0,0 +1,377 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::Companies::CreateService do + subject(:service_call) { described_class.call(integration:, customer:, subsidiary_id:) } + + let(:service) { described_class.new(integration:, customer:, subsidiary_id:) } + let(:customer) { create(:customer, :with_same_billing_and_shipping_address, organization:) } + let(:subsidiary_id) { '1' } + let(:organization) { create(:organization) } + let(:lago_client) { instance_double(LagoHttpClient::Client) } + let(:endpoint) { "https://api.nango.dev/v1/#{integration_type}/companies" } + + let(:headers) do + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{ENV["NANGO_SECRET_KEY"]}", + 'Provider-Config-Key' => integration_type_key + } + end + + let(:customer_link) do + url = ENV["LAGO_FRONT_URL"].presence || "https://app.getlago.com" + + URI.join(url, "/customer/", customer.id).to_s + end + + before do + allow(LagoHttpClient::Client).to receive(:new).with(endpoint).and_return(lago_client) + end + + describe '#initialize' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + context 'when customer is a company' do + let(:customer) do + create(:customer, :with_same_billing_and_shipping_address, organization:, customer_type: 'company') + end + + it 'initializes the service without errors' do + expect { described_class.new(integration:, customer:, subsidiary_id:) }.not_to raise_error + end + end + end + + describe '#call' do + context 'when service call is successful' do + let(:response) { instance_double(Net::HTTPOK) } + + before do + allow(lago_client).to receive(:post_with_response).with(params, headers).and_return(response) + allow(response).to receive(:body).and_return(body) + end + + context 'when response is a string' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + end + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/companies/success_hash_response.json') + File.read(path) + end + + it 'returns contact id' do + result = service_call + + aggregate_failures do + expect(result).to be_success + expect(result.contact_id).to eq('2e50c200-9a54-4a66-b241-1e75fb87373f') + expect(result.email).to eq('roger@rogers.com') + end + end + + it 'delivers a success webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_created', + customer + ).on_queue(:webhook) + end + end + + context 'when response is a hash' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + end + + context 'when contact is succesfully created' do + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/companies/success_hash_response.json') + File.read(path) + end + + it 'returns contact id' do + result = service_call + + aggregate_failures do + expect(result).to be_success + expect(result.contact_id).to eq('2e50c200-9a54-4a66-b241-1e75fb87373f') + end + end + + it 'delivers a success webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_created', + customer + ).on_queue(:webhook) + end + end + + context 'when contact is not created' do + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/companies/failure_hash_response.json') + File.read(path) + end + + it 'does not return contact id' do + result = service_call + + aggregate_failures do + expect(result).to be_success + expect(result.contact).to be(nil) + end + end + + it 'does not create integration resource object' do + expect { service_call }.not_to change(IntegrationResource, :count) + end + end + end + end + + context 'when service call is not successful' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + end + + let(:http_error) { LagoHttpClient::HttpError.new(error_code, body, nil) } + + before do + allow(lago_client).to receive(:post_with_response).with(params, headers).and_raise(http_error) + end + + context 'when it is a server error' do + let(:error_code) { Faker::Number.between(from: 500, to: 599) } + let(:code) { 'action_script_runtime_error' } + let(:message) { 'submitFields: Missing a required argument: type' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq(code) + expect(result.error.message).to eq("#{code}: #{message}") + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message:, + error_code: code + } + ) + end + end + + context 'when it is a server payload error' do + let(:error_code) { Faker::Number.between(from: 500, to: 599) } + let(:code) { 'TypeError' } + let(:message) { 'Please enter value(s) for: Company Name' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_payload_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq(code) + expect(result.error.message).to eq("#{code}: #{message}") + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message:, + error_code: code + } + ) + end + end + + context 'when it is a client error' do + let(:error_code) { 404 } + let(:code) { 'invalid_secret_key_format' } + let(:message) { 'Authentication failed. The provided secret key is not a UUID v4.' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_auth_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq(code) + expect(result.error.message).to eq("#{code}: #{message}") + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message:, + error_code: code + } + ) + end + end + end + end + + describe '#process_hash_result' do + subject(:process_hash_result) { service.send(:process_hash_result, body) } + + let(:result) { service.instance_variable_get(:@result) } + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + before do + allow(service).to receive(:deliver_error_webhook) + end + + context 'when contact is successfully created' do + let(:body) do + { + 'succeededCompanies' => [ + { + 'id' => '2e50c200-9a54-4a66-b241-1e75fb87373f', + 'email' => 'billing@example.com' + } + ] + } + end + + it 'sets the contact_id and email in the result' do + process_hash_result + + expect(result.contact_id).to eq('2e50c200-9a54-4a66-b241-1e75fb87373f') + expect(result.email).to eq('billing@example.com') + end + end + + context 'when contact creation fails' do + let(:body) do + { + 'failedCompanies' => [ + { + 'validation_errors' => [ + {'Message' => 'Email is invalid'}, + {'Message' => 'Name is required'} + ] + } + ] + } + end + + it 'delivers an error webhook' do + process_hash_result + + expect(service).to have_received(:deliver_error_webhook).with( + customer:, + code: 'Validation error', + message: 'Email is invalid. Name is required' + ) + end + end + + context 'when there is a general error' do + let(:body) do + { + 'error' => { + 'payload' => { + 'message' => 'An unexpected error occurred' + } + } + } + end + + it 'delivers an error webhook' do + process_hash_result + + expect(service).to have_received(:deliver_error_webhook).with( + customer:, + code: 'Validation error', + message: 'An unexpected error occurred' + ) + end + end + end +end diff --git a/spec/services/integrations/aggregator/companies/payloads/factory_spec.rb b/spec/services/integrations/aggregator/companies/payloads/factory_spec.rb new file mode 100644 index 00000000000..5414327faeb --- /dev/null +++ b/spec/services/integrations/aggregator/companies/payloads/factory_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::Companies::Payloads::Factory do + let(:customer) { integration_customer.customer } + let(:integration) { integration_customer.integration } + let(:subsidiary_id) { '123' } + + describe '.new_instance' do + subject(:new_instance_call) do + described_class.new_instance(integration:, customer:, integration_customer:, subsidiary_id:) + end + + context 'when customer is a hubspot customer' do + let(:integration_customer) { FactoryBot.create(:hubspot_customer, customer:) } + let(:customer) { FactoryBot.create(:customer) } + let(:customer_type) { ['company', nil].sample } + + it 'returns payload' do + expect(subject).to be_a(Integrations::Aggregator::Companies::Payloads::Hubspot) + end + end + end +end diff --git a/spec/services/integrations/aggregator/companies/payloads/hubspot_spec.rb b/spec/services/integrations/aggregator/companies/payloads/hubspot_spec.rb new file mode 100644 index 00000000000..76ae5167b49 --- /dev/null +++ b/spec/services/integrations/aggregator/companies/payloads/hubspot_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::Companies::Payloads::Hubspot do + let(:integration) { integration_customer.integration } + let(:integration_customer) { FactoryBot.create(:hubspot_customer, customer:) } + let(:customer) { create(:customer, customer_type: 'company') } + let(:payload) { described_class.new(integration:, customer:, integration_customer:) } + let(:customer_link) { payload.__send__(:customer_url) } + + describe "#create_body" do + subject(:create_body_call) { payload.create_body } + + let(:payload_body) do + { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => customer_link + } + } + end + + it 'returns the payload body' do + expect(subject).to eq payload_body + end + end + + describe "#update_body" do + subject(:update_body_call) { payload.update_body } + + let(:payload_body) do + { + 'companyId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => customer_link + } + } + } + end + + it 'returns the payload body' do + expect(subject).to eq payload_body + end + end +end diff --git a/spec/services/integrations/aggregator/companies/update_service_spec.rb b/spec/services/integrations/aggregator/companies/update_service_spec.rb new file mode 100644 index 00000000000..20c7d982ce7 --- /dev/null +++ b/spec/services/integrations/aggregator/companies/update_service_spec.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::Companies::UpdateService do + subject(:service_call) { described_class.call(integration:, integration_customer:) } + + let(:customer) { create(:customer, organization:) } + let(:organization) { create(:organization) } + let(:lago_client) { instance_double(LagoHttpClient::Client) } + let(:endpoint) { "https://api.nango.dev/v1/#{integration_type}/companies" } + + let(:headers) do + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{ENV["NANGO_SECRET_KEY"]}", + 'Provider-Config-Key' => integration_type_key + } + end + + let(:customer_link) do + url = ENV["LAGO_FRONT_URL"].presence || "https://app.getlago.com" + + URI.join(url, "/customer/", customer.id).to_s + end + + before do + allow(LagoHttpClient::Client).to receive(:new).with(endpoint).and_return(lago_client) + end + + describe '#initialize' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_customer) { create(:hubspot_customer, integration:, customer:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + context 'when integration customer is a company' do + it 'initializes the service without errors' do + expect { described_class.new(integration:, integration_customer:) }.not_to raise_error + end + end + + context 'when integration customer is an individual' do + before do + allow(integration_customer.customer).to receive(:customer_type_individual?).and_return(true) + end + + it 'raises an ArgumentError' do + expect { described_class.new(integration:, integration_customer:) } + .to raise_error(ArgumentError, 'Integration customer is not a company') + end + end + end + + describe '#call' do + context 'when service call is successful' do + let(:response) { instance_double(Net::HTTPOK) } + let(:code) { 200 } + + context 'when response is a string' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_customer) { create(:hubspot_customer, integration:, customer:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'companyId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + } + end + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/companies/success_string_response.json') + File.read(path) + end + + before do + allow(lago_client).to receive(:put_with_response).with(params, headers).and_return(response) + allow(response).to receive(:body).and_return(body) + end + + it 'returns contact id' do + result = service_call + + aggregate_failures do + expect(result).to be_success + expect(result.contact_id).to eq('1') + end + end + end + + context 'when response is a hash' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_customer) { create(:hubspot_customer, integration:, customer:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'companyId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + } + end + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/companies/success_hash_response.json') + File.read(path) + end + + before do + allow(lago_client).to receive(:put_with_response).with(params, headers).and_return(response) + allow(response).to receive(:body).and_return(body) + end + + it 'returns contact id' do + result = service_call + + aggregate_failures do + expect(result).to be_success + expect(result.contact_id).to eq('2e50c200-9a54-4a66-b241-1e75fb87373f') + end + end + end + end + + context 'when service call is not successful' do + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_customer) { create(:hubspot_customer, integration:, customer:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + let(:params) do + { + 'companyId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'name' => customer.name, + 'domain' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_tax_identification_number' => customer.tax_identification_number, + 'lago_customer_link' => anything + } + } + } + end + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_response.json') + File.read(path) + end + + let(:http_error) { LagoHttpClient::HttpError.new(error_code, body, nil) } + + before do + allow(lago_client).to receive(:put_with_response).with(params, headers).and_raise(http_error) + end + + context 'when it is a server error' do + let(:error_code) { Faker::Number.between(from: 500, to: 599) } + let(:code) { 'action_script_runtime_error' } + let(:message) { 'submitFields: Missing a required argument: type' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq('action_script_runtime_error') + expect(result.error.message) + .to eq('action_script_runtime_error: submitFields: Missing a required argument: type') + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message: 'submitFields: Missing a required argument: type', + error_code: 'action_script_runtime_error' + } + ) + end + end + + context 'when it is a server payload error' do + let(:error_code) { Faker::Number.between(from: 500, to: 599) } + let(:code) { 'TypeError' } + let(:message) { 'Please enter value(s) for: Company Name' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_payload_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq(code) + expect(result.error.message).to eq("#{code}: #{message}") + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message:, + error_code: code + } + ) + end + end + + context 'when it is a client error' do + let(:error_code) { 404 } + let(:code) { 'invalid_secret_key_format' } + let(:message) { 'Authentication failed. The provided secret key is not a UUID v4.' } + + let(:body) do + path = Rails.root.join('spec/fixtures/integration_aggregator/error_auth_response.json') + File.read(path) + end + + it 'returns an error' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error.code).to eq(code) + expect(result.error.message).to eq("#{code}: #{message}") + end + end + + it 'delivers an error webhook' do + expect { service_call }.to enqueue_job(SendWebhookJob) + .with( + 'customer.crm_provider_error', + customer, + provider: 'hubspot', + provider_code: integration.code, + provider_error: { + message:, + error_code: code + } + ) + end + end + end + end +end diff --git a/spec/services/integrations/aggregator/contacts/create_service_spec.rb b/spec/services/integrations/aggregator/contacts/create_service_spec.rb index 250cd3212df..7065f806028 100644 --- a/spec/services/integrations/aggregator/contacts/create_service_spec.rb +++ b/spec/services/integrations/aggregator/contacts/create_service_spec.rb @@ -5,6 +5,7 @@ RSpec.describe Integrations::Aggregator::Contacts::CreateService do subject(:service_call) { described_class.call(integration:, customer:, subsidiary_id:) } + let(:service) { described_class.new(integration:, customer:, subsidiary_id:) } let(:customer) { create(:customer, :with_same_billing_and_shipping_address, organization:) } let(:subsidiary_id) { '1' } let(:organization) { create(:organization) } @@ -330,4 +331,84 @@ end end end + + describe '#process_hash_result' do + subject(:process_hash_result) { service.send(:process_hash_result, body) } + + let(:result) { service.instance_variable_get(:@result) } + let(:integration) { create(:hubspot_integration, organization:) } + let(:integration_type) { 'hubspot' } + let(:integration_type_key) { 'hubspot' } + + before do + allow(service).to receive(:deliver_error_webhook) + end + + context 'when contact is successfully created' do + let(:body) do + { + 'succeededContacts' => [ + { + 'id' => '2e50c200-9a54-4a66-b241-1e75fb87373f', + 'email' => 'billing@example.com' + } + ] + } + end + + it 'sets the contact_id and email in the result' do + process_hash_result + + expect(result.contact_id).to eq('2e50c200-9a54-4a66-b241-1e75fb87373f') + expect(result.email).to eq('billing@example.com') + end + end + + context 'when contact creation fails' do + let(:body) do + { + 'failedContacts' => [ + { + 'validation_errors' => [ + {'Message' => 'Email is invalid'}, + {'Message' => 'Name is required'} + ] + } + ] + } + end + + it 'delivers an error webhook' do + process_hash_result + + expect(service).to have_received(:deliver_error_webhook).with( + customer:, + code: 'Validation error', + message: 'Email is invalid. Name is required' + ) + end + end + + context 'when there is a general error' do + let(:body) do + { + 'error' => { + 'payload' => { + 'message' => 'An unexpected error occurred' + } + } + } + end + + it 'delivers an error webhook' do + process_hash_result + + expect(service).to have_received(:deliver_error_webhook).with( + customer:, + code: 'Validation error', + message: 'An unexpected error occurred' + ) + end + end + end end diff --git a/spec/services/integrations/aggregator/contacts/payloads/factory_spec.rb b/spec/services/integrations/aggregator/contacts/payloads/factory_spec.rb index 5f5b5acca03..68df5140b7a 100644 --- a/spec/services/integrations/aggregator/contacts/payloads/factory_spec.rb +++ b/spec/services/integrations/aggregator/contacts/payloads/factory_spec.rb @@ -35,6 +35,16 @@ expect(subject).to be_a(Integrations::Aggregator::Contacts::Payloads::Anrok) end end + + context 'when customer is a hubspot customer' do + let(:integration_customer) { FactoryBot.create(:hubspot_customer, customer:) } + let(:customer) { FactoryBot.create(:customer, customer_type:) } + let(:customer_type) { 'individual' } + + it 'returns payload' do + expect(subject).to be_a(Integrations::Aggregator::Contacts::Payloads::Hubspot) + end + end end describe '#create_body' do diff --git a/spec/services/integrations/aggregator/contacts/payloads/hubspot_spec.rb b/spec/services/integrations/aggregator/contacts/payloads/hubspot_spec.rb new file mode 100644 index 00000000000..39886329137 --- /dev/null +++ b/spec/services/integrations/aggregator/contacts/payloads/hubspot_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::Contacts::Payloads::Hubspot do + let(:integration) { integration_customer.integration } + let(:integration_customer) { FactoryBot.create(:hubspot_customer, customer:) } + let(:customer) { create(:customer, customer_type: 'individual') } + let(:payload) { described_class.new(integration:, customer:, integration_customer:) } + let(:customer_link) { payload.__send__(:customer_url) } + + describe "#create_body" do + subject(:create_body_call) { payload.create_body } + + let(:payload_body) do + { + 'properties' => { + 'email' => customer.email, + 'firstname' => customer.firstname, + 'lastname' => customer.lastname, + 'phone' => customer.phone, + 'company' => customer.legal_name, + 'website' => customer.url, + 'lago_customer_id' => customer.id, + 'lago_customer_external_id' => customer.external_id, + 'lago_billing_email' => customer.email, + 'lago_customer_link' => customer_link + } + } + end + + it 'returns the payload body' do + expect(subject).to eq payload_body + end + end + + describe "#update_body" do + subject(:update_body_call) { payload.update_body } + + let(:payload_body) do + { + 'contactId' => integration_customer.external_customer_id, + 'input' => { + 'properties' => { + 'email' => customer.email, + 'firstname' => customer.firstname, + 'lastname' => customer.lastname, + 'phone' => customer.phone, + 'company' => customer.legal_name, + 'website' => customer.url + } + } + } + end + + it 'returns the payload body' do + expect(subject).to eq payload_body + end + end +end diff --git a/spec/services/integrations/aggregator/custom_object_service_spec.rb b/spec/services/integrations/aggregator/custom_object_service_spec.rb new file mode 100644 index 00000000000..43285258e01 --- /dev/null +++ b/spec/services/integrations/aggregator/custom_object_service_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Aggregator::CustomObjectService do + subject(:custom_object_service) { described_class.new(integration:, name:) } + + let(:integration) { create(:hubspot_integration) } + let(:name) { 'LagoInvoices' } + + describe '.call' do + let(:lago_client) { instance_double(LagoHttpClient::Client) } + let(:endpoint) { 'https://api.nango.dev/v1/hubspot/custom-object' } + + let(:headers) do + { + 'Connection-Id' => integration.connection_id, + 'Authorization' => "Bearer #{ENV["NANGO_SECRET_KEY"]}", + 'Provider-Config-Key' => 'hubspot' + } + end + + let(:body) do + { + 'name' => name + } + end + + let(:aggregator_response) do + path = Rails.root.join('spec/fixtures/integration_aggregator/custom_object_response.json') + JSON.parse(File.read(path)) + end + + before do + allow(LagoHttpClient::Client).to receive(:new).with(endpoint).and_return(lago_client) + allow(lago_client).to receive(:get).with(headers:, body:).and_return(aggregator_response) + end + + it 'successfully fetches custom object' do + result = custom_object_service.call + custom_object = result.custom_object + + aggregate_failures do + expect(LagoHttpClient::Client).to have_received(:new).with(endpoint) + expect(lago_client).to have_received(:get) + expect(custom_object.id).to eq('35482707') + expect(custom_object.objectTypeId).to eq('2-35482707') + end + end + end +end diff --git a/spec/services/integrations/aggregator/send_private_app_token_service_spec.rb b/spec/services/integrations/aggregator/send_private_app_token_service_spec.rb deleted file mode 100644 index 7b67e366ba2..00000000000 --- a/spec/services/integrations/aggregator/send_private_app_token_service_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Integrations::Aggregator::SendPrivateAppTokenService do - subject(:send_private_token_service) { described_class.new(integration:) } - - let(:integration) { create(:hubspot_integration) } - - describe '.call' do - let(:lago_client) { instance_double(LagoHttpClient::Client) } - let(:endpoint) { "https://api.nango.dev/connection/#{integration.connection_id}/metadata" } - - before do - allow(LagoHttpClient::Client).to receive(:new) - .with(endpoint) - .and_return(lago_client) - allow(lago_client).to receive(:post_with_response) - - integration.private_app_token = 'privatetoken' - integration.save! - end - - it 'successfully sends token to nango' do - send_private_token_service.call - - aggregate_failures do - expect(LagoHttpClient::Client).to have_received(:new) - .with(endpoint) - expect(lago_client).to have_received(:post_with_response) do |payload| - expect(payload[:privateAppToken]).to eq('privatetoken') - end - end - end - end -end diff --git a/spec/services/integrations/hubspot/create_service_spec.rb b/spec/services/integrations/hubspot/create_service_spec.rb index 9c028ac635a..c08eda9cb06 100644 --- a/spec/services/integrations/hubspot/create_service_spec.rb +++ b/spec/services/integrations/hubspot/create_service_spec.rb @@ -18,7 +18,6 @@ code: 'hubspot1', organization_id: organization.id, connection_id: 'conn1', - private_app_token: 'token', client_secret: 'secret', default_targeted_object: "test", sync_invoices: false, @@ -58,8 +57,8 @@ context 'with hubspot premium integration present' do before do organization.update!(premium_integrations: ['hubspot']) - allow(Integrations::Aggregator::SendPrivateAppTokenJob).to receive(:perform_later) allow(Integrations::Aggregator::SyncCustomObjectsAndPropertiesJob).to receive(:perform_later) + allow(Integrations::Hubspot::SavePortalIdJob).to receive(:perform_later) end context 'without validation errors' do @@ -70,7 +69,6 @@ expect(integration.name).to eq(name) expect(integration.code).to eq(create_args[:code]) expect(integration.connection_id).to eq(create_args[:connection_id]) - expect(integration.private_app_token).to eq(create_args[:private_app_token]) expect(integration.default_targeted_object).to eq(create_args[:default_targeted_object]) expect(integration.sync_invoices).to eq(create_args[:sync_invoices]) expect(integration.sync_subscriptions).to eq(create_args[:sync_subscriptions]) @@ -87,8 +85,8 @@ service_call integration = Integrations::HubspotIntegration.order(:created_at).last - expect(Integrations::Aggregator::SendPrivateAppTokenJob).to have_received(:perform_later).with(integration:) expect(Integrations::Aggregator::SyncCustomObjectsAndPropertiesJob).to have_received(:perform_later).with(integration:) + expect(Integrations::Hubspot::SavePortalIdJob).to have_received(:perform_later).with(integration:) end end diff --git a/spec/services/integrations/hubspot/invoices/deploy_object_service_spec.rb b/spec/services/integrations/hubspot/invoices/deploy_object_service_spec.rb index b9ef309ec38..ca3bc7284bb 100644 --- a/spec/services/integrations/hubspot/invoices/deploy_object_service_spec.rb +++ b/spec/services/integrations/hubspot/invoices/deploy_object_service_spec.rb @@ -9,14 +9,26 @@ describe '.call' do let(:http_client) { instance_double(LagoHttpClient::Client) } + let(:http_client_get) { instance_double(LagoHttpClient::Client) } let(:endpoint) { "https://api.nango.dev/v1/hubspot/object" } + let(:customer_object_endpoint) { "https://api.nango.dev/v1/hubspot/custom-object" } let(:response) { instance_double('Response', success?: true) } + let(:get_response) do + path = Rails.root.join('spec/fixtures/integration_aggregator/custom_object_response.json') + JSON.parse(File.read(path)) + end + before do allow(LagoHttpClient::Client).to receive(:new) .with(endpoint) .and_return(http_client) + allow(LagoHttpClient::Client).to receive(:new) + .with(customer_object_endpoint) + .and_return(http_client_get) allow(http_client).to receive(:post_with_response).and_return(response) + allow(http_client_get).to receive(:get).and_raise LagoHttpClient::HttpError.new('error', 'error', nil) + allow(response).to receive(:[]).with('objectTypeId').and_return('123') integration.invoices_properties_version = nil integration.save! @@ -52,6 +64,50 @@ end end + context 'when custom object service returns a valid objectTypeId' do + let(:custom_object_result) do + instance_double( + 'CustomObjectResult', + success?: true, + custom_object: instance_double('CustomObject', objectTypeId: '123') + ) + end + + before do + allow(http_client).to receive(:get).and_return(get_response) + allow(Integrations::Aggregator::CustomObjectService).to receive(:call).and_return(custom_object_result) + end + + it 'saves the objectTypeId and updates the invoices_properties_version' do + deploy_object_service.call + + aggregate_failures do + expect(integration.reload.invoices_object_type_id).to eq('123') + expect(integration.reload.invoices_properties_version).to eq(described_class::VERSION) + end + end + end + + context 'when custom object service does not return a valid objectTypeId' do + let(:custom_object_result) do + instance_double('CustomObjectResult', success?: false) + end + + before do + allow(Integrations::Aggregator::CustomObjectService).to receive(:call).and_return(custom_object_result) + end + + it 'makes an API call and updates the invoices_properties_version' do + deploy_object_service.call + + aggregate_failures do + expect(LagoHttpClient::Client).to have_received(:new).with(endpoint) + expect(http_client).to have_received(:post_with_response) + expect(integration.reload.invoices_properties_version).to eq(described_class::VERSION) + end + end + end + context 'when an HTTP error occurs' do let(:error) { LagoHttpClient::HttpError.new('error message', '{"error": {"message": "unknown failure"}}', nil) } diff --git a/spec/services/integrations/hubspot/save_portal_id_service_spec.rb b/spec/services/integrations/hubspot/save_portal_id_service_spec.rb new file mode 100644 index 00000000000..58df85a166a --- /dev/null +++ b/spec/services/integrations/hubspot/save_portal_id_service_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Integrations::Hubspot::SavePortalIdService do + describe '#call' do + let(:portal_id) { '123456' } + let(:integration) { create(:hubspot_integration) } + let(:service_call) { described_class.call(integration:) } + let(:result) { BaseService::Result.new } + let(:account_information) { OpenStruct.new(id: portal_id) } + + before do + result.account_information = account_information + allow(Integrations::Aggregator::AccountInformationService).to receive(:call).and_return(result) + end + + context 'when the service is successful' do + it 'saves the portal ID to the integration' do + expect { service_call }.to change { integration.reload.portal_id }.to(portal_id) + end + + it 'returns a success result' do + result = service_call + expect(result).to be_success + end + end + + context 'when the service fails' do + before do + allow(integration).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(integration)) + end + + it 'does not change the portal ID' do + expect { service_call }.not_to change { integration.reload.portal_id } + end + + it 'returns an error message' do + result = service_call + + aggregate_failures do + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::ValidationFailure) + end + end + end + end +end diff --git a/spec/services/integrations/hubspot/subscriptions/deploy_object_service_spec.rb b/spec/services/integrations/hubspot/subscriptions/deploy_object_service_spec.rb index b2943ae8339..acc2ecfe06b 100644 --- a/spec/services/integrations/hubspot/subscriptions/deploy_object_service_spec.rb +++ b/spec/services/integrations/hubspot/subscriptions/deploy_object_service_spec.rb @@ -17,6 +17,7 @@ .with(endpoint) .and_return(http_client) allow(http_client).to receive(:post_with_response).and_return(response) + allow(response).to receive(:[]).with('objectTypeId').and_return('123') integration.subscriptions_properties_version = nil integration.save! diff --git a/spec/services/integrations/hubspot/update_service_spec.rb b/spec/services/integrations/hubspot/update_service_spec.rb index b2bf405a1c4..de2d717c4b5 100644 --- a/spec/services/integrations/hubspot/update_service_spec.rb +++ b/spec/services/integrations/hubspot/update_service_spec.rb @@ -16,8 +16,7 @@ let(:update_args) do { name:, - code: 'hubspot1', - private_app_token: 'new_token' + code: 'hubspot1' } end @@ -49,7 +48,6 @@ context 'with hubspot premium integration present' do before do organization.update!(premium_integrations: ['hubspot']) - allow(Integrations::Aggregator::SendPrivateAppTokenJob).to receive(:perform_later) end context 'without validation errors' do @@ -58,7 +56,6 @@ integration = Integrations::HubspotIntegration.order(:updated_at).last expect(integration.name).to eq(name) - expect(integration.private_app_token).to eq(update_args[:private_app_token]) end it 'returns an integration in result object' do @@ -66,12 +63,6 @@ expect(result.integration).to be_a(Integrations::HubspotIntegration) end - - it 'calls Integrations::Aggregator::SendPrivateAppTokenJob' do - service_call - - expect(Integrations::Aggregator::SendPrivateAppTokenJob).to have_received(:perform_later).with(integration:) - end end context 'with validation error' do diff --git a/spec/services/webhooks/integrations/customer_created_service_spec.rb b/spec/services/webhooks/integrations/accounting_customer_created_service_spec.rb similarity index 84% rename from spec/services/webhooks/integrations/customer_created_service_spec.rb rename to spec/services/webhooks/integrations/accounting_customer_created_service_spec.rb index ea32cdacb83..326afebcab7 100644 --- a/spec/services/webhooks/integrations/customer_created_service_spec.rb +++ b/spec/services/webhooks/integrations/accounting_customer_created_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Webhooks::Integrations::CustomerCreatedService do +RSpec.describe Webhooks::Integrations::AccountingCustomerCreatedService do subject(:webhook_service) { described_class.new(object: customer) } let(:customer) { create(:customer, organization:) } diff --git a/spec/services/webhooks/integrations/customer_error_service_spec.rb b/spec/services/webhooks/integrations/accounting_customer_error_service_spec.rb similarity index 86% rename from spec/services/webhooks/integrations/customer_error_service_spec.rb rename to spec/services/webhooks/integrations/accounting_customer_error_service_spec.rb index 86cef9453b7..f9a19e6edf8 100644 --- a/spec/services/webhooks/integrations/customer_error_service_spec.rb +++ b/spec/services/webhooks/integrations/accounting_customer_error_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Webhooks::Integrations::CustomerErrorService do +RSpec.describe Webhooks::Integrations::AccountingCustomerErrorService do subject(:webhook_service) { described_class.new(object: customer, options: webhook_options) } let(:customer) { create(:customer, organization:) } diff --git a/spec/services/webhooks/integrations/crm_customer_created_service_spec.rb b/spec/services/webhooks/integrations/crm_customer_created_service_spec.rb new file mode 100644 index 00000000000..164c809845b --- /dev/null +++ b/spec/services/webhooks/integrations/crm_customer_created_service_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Webhooks::Integrations::CrmCustomerCreatedService do + subject(:webhook_service) { described_class.new(object: customer) } + + let(:customer) { create(:customer, organization:) } + let(:organization) { create(:organization) } + + describe '.call' do + it_behaves_like 'creates webhook', + 'customer.crm_provider_created', + 'customer', + {'integration_customers' => []} + end +end diff --git a/spec/services/webhooks/integrations/crm_customer_error_service_spec.rb b/spec/services/webhooks/integrations/crm_customer_error_service_spec.rb new file mode 100644 index 00000000000..78fa42f5736 --- /dev/null +++ b/spec/services/webhooks/integrations/crm_customer_error_service_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Webhooks::Integrations::CrmCustomerErrorService do + subject(:webhook_service) { described_class.new(object: customer, options: webhook_options) } + + let(:customer) { create(:customer, organization:) } + let(:organization) { create(:organization) } + let(:webhook_options) { {provider_error: {message: 'message', error_code: 'code'}} } + + describe '.call' do + it_behaves_like 'creates webhook', 'customer.crm_provider_error', 'crm_provider_customer_error' + end +end