Skip to content

Commit

Permalink
feat: add hubspot connection services (#2597)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunomiguelpinto committed Sep 18, 2024
1 parent 76a0dbf commit a66c402
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 0 deletions.
16 changes: 16 additions & 0 deletions app/jobs/integrations/aggregator/send_private_app_token_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Integrations
module Aggregator
class SendPrivateAppTokenJob < ApplicationJob
queue_as 'integrations'

retry_on LagoHttpClient::HttpError, wait: :polynomially_longer, attempts: 3

def perform(integration:)
result = Integrations::Aggregator::SendPrivateAppTokenService.call(integration:)
result.raise_if_error!
end
end
end
end
4 changes: 4 additions & 0 deletions app/services/integrations/aggregator/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def provider
'xero'
when 'Integrations::AnrokIntegration'
'anrok'
when 'Integrations::HubspotIntegration'
'hubspot'
end
end

Expand All @@ -43,6 +45,8 @@ def provider_key
'xero'
when 'Integrations::AnrokIntegration'
'anrok'
when 'Integrations::HubspotIntegration'
'hubspot'
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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
44 changes: 44 additions & 0 deletions app/services/integrations/hubspot/create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Integrations
module Hubspot
class CreateService < BaseService
attr_reader :params
def initialize(params:)
@params = params

super
end

def call
organization = Organization.find_by(id: params[:organization_id])

unless organization.premium_integrations.include?('hubspot')
return result.not_allowed_failure!(code: 'premium_integration_missing')
end

integration = Integrations::HubspotIntegration.new(
organization:,
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])
)

integration.save!

if integration.type == 'Integrations::HubspotIntegration'
Integrations::Aggregator::SendPrivateAppTokenJob.perform_later(integration:)
end

result.integration = integration
result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end
end
end
end
46 changes: 46 additions & 0 deletions app/services/integrations/hubspot/update_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Integrations
module Hubspot
class UpdateService < BaseService
def initialize(integration:, params:)
@integration = integration
@params = params

super
end

def call
return result.not_found_failure!(resource: 'integration') unless integration

unless integration.organization.premium_integrations.include?('hubspot')
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
result.record_validation_failure!(record: e.record)
end

private

attr_reader :integration, :params
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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
109 changes: 109 additions & 0 deletions spec/services/integrations/hubspot/create_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Integrations::Hubspot::CreateService, type: :service do
let(:membership) { create(:membership) }
let(:organization) { membership.organization }

describe '#call' do
subject(:service_call) { described_class.call(params: create_args) }

let(:name) { 'Hubspot 1' }
let(:script_endpoint_url) { Faker::Internet.url }

let(:create_args) do
{
name:,
code: 'hubspot1',
organization_id: organization.id,
connection_id: 'conn1',
private_app_token: 'token',
client_secret: 'secret',
default_targeted_object: "test",
sync_invoices: false,
sync_subscriptions: false
}
end

context 'without premium license' do
it 'does not create an integration' do
expect { service_call }.not_to change(Integrations::HubspotIntegration, :count)
end

it 'returns an error' do
result = service_call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::MethodNotAllowedFailure)
end
end
end

context 'with premium license' do
around { |test| lago_premium!(&test) }

context 'with hubspot premium integration not present' do
it 'returns an error' do
result = service_call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::MethodNotAllowedFailure)
end
end
end

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
it 'creates an integration' do
expect { service_call }.to change(Integrations::HubspotIntegration, :count).by(1)

integration = Integrations::HubspotIntegration.order(:created_at).last
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])
expect(integration.organization_id).to eq(organization.id)
end

it 'returns an integration in result object' do
result = service_call

expect(result.integration).to be_a(Integrations::HubspotIntegration)
end

it 'calls Integrations::Aggregator::SendPrivateAppTokenJob' do
service_call

integration = Integrations::HubspotIntegration.order(:created_at).last
expect(Integrations::Aggregator::SendPrivateAppTokenJob).to have_received(:perform_later).with(integration:)
end
end

context 'with validation error' do
let(:name) { nil }

it 'returns an error' do
result = service_call

aggregate_failures do
expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
expect(result.error.messages[:name]).to eq(['value_is_mandatory'])
end
end
end
end
end
end
end
Loading

0 comments on commit a66c402

Please sign in to comment.