Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=deterministic-key
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=derivation-salt

EDITOR_ENCRYPTION_KEY=a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0

SALESFORCE_USERNAME=salesforce-username
SALESFORCE_PASSWORD=salesforce-password
SALESFORCE_CLIENT_ID=salesforce-client-id
SALESFORCE_CLIENT_SECRET=salesforce-client-secret
SALESFORCE_HOST=salesforce-host
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ gem 'postmark-rails'
gem 'puma', '~> 6'
gem 'rack-cors'
gem 'rails', '~> 7.1'
gem 'restforce', '~> 8.0'
gem 'scout_apm'
gem 'sentry-rails'

Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ GEM
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (3.0.2)
ffi (1.16.3)
fugit (1.11.1)
Expand Down Expand Up @@ -254,6 +258,7 @@ GEM
minitest (5.23.1)
msgpack (1.6.0)
multi_xml (0.6.0)
multipart-post (2.4.1)
mutex_m (0.2.0)
net-imap (0.4.12)
date
Expand Down Expand Up @@ -376,6 +381,13 @@ GEM
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
restforce (8.0.0)
faraday (>= 1.1.0, < 3.0.0)
faraday-follow_redirects (<= 0.3.0, < 1.0.0)
faraday-multipart (>= 1.0.0, < 2.0.0)
faraday-net_http (< 4.0.0)
hashie (>= 1.2.0, < 6.0)
jwt (>= 1.5.6)
rexml (3.3.0)
strscan
rspec (3.12.0)
Expand Down Expand Up @@ -559,6 +571,7 @@ DEPENDENCIES
rack-cors
rails (~> 7.1)
rails-erd
restforce (~> 8.0)
rspec
rspec-rails
rspec_junit_formatter
Expand Down
33 changes: 33 additions & 0 deletions app/jobs/salesforce_sync_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

class SalesforceSyncJob < ApplicationJob
def perform(school_id)
school = School.find(school_id)

account_data = {
Name: school.name,
Website: school.website,
BillingStreet: [school.address_line_1, school.address_line_2].compact.join("\n"),
BillingCity: school.municipality,
BillingState: school.administrative_area,
BillingPostalCode: school.postal_code,
BillingCountryCode: school.country_code,
Industry: 'Education'
}

client.create('Account', account_data)
end
Comment on lines +18 to +19
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Salesforce API call has no error handling. If the API call fails, the job will raise an exception with no context about which school failed to sync. Consider wrapping this in a rescue block that logs the school_id and re-raises, or let the job framework handle retries with better error context.

Suggested change
client.create('Account', account_data)
end
begin
client.create('Account', account_data)
rescue => e
Rails.logger.error("SalesforceSyncJob failed for school_id=#{school_id}: #{e.class} - #{e.message}")
raise
end

Copilot uses AI. Check for mistakes.

private

def client
Restforce.new(
username: ENV.fetch('SALESFORCE_USERNAME'),
password: ENV.fetch('SALESFORCE_PASSWORD'),
client_id: ENV.fetch('SALESFORCE_CLIENT_ID'),
client_secret: ENV.fetch('SALESFORCE_CLIENT_SECRET'),
host: ENV.fetch('SALESFORCE_HOST'),
api_version: '57.0'
)
end
Comment on lines +23 to +32
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Restforce client is being instantiated on every job execution. Consider memoizing the client with @client ||= to avoid recreating the connection for potential retry scenarios.

Copilot uses AI. Check for mistakes.
end
4 changes: 4 additions & 0 deletions app/models/school.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ def verify!
attempts += 1
retry
end

SalesforceSyncJob.perform_later(id)

true
Comment on lines +69 to +70
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The explicit true return value is unnecessary since perform_later already returns a truthy value. If an explicit boolean return is required for the method contract, consider documenting why this is needed.

Suggested change
true

Copilot uses AI. Check for mistakes.
end

def reject
Expand Down
79 changes: 79 additions & 0 deletions spec/jobs/salesforce_sync_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SalesforceSyncJob do
describe '#perform' do
let(:school) do
create(:school,
name: 'West Beverly Hills High School',
website: 'https://example.com',
address_line_1: '16711 Mulholland Drive',
address_line_2: nil,
municipality: 'Beverly Hills',
administrative_area: 'California',
postal_code: '90210',
country_code: 'US')
end

let(:mock_client) { instance_double(Restforce::Client) }

let(:expected_account_data) do
{
Name: 'West Beverly Hills High School',
Website: 'https://example.com',
BillingStreet: '16711 Mulholland Drive',
BillingCity: 'Beverly Hills',
BillingState: 'California',
BillingPostalCode: '90210',
BillingCountryCode: 'US',
Industry: 'Education'
}
end

before do
stub_const('ENV', {
'SALESFORCE_USERNAME' => 'salesforce-username',
'SALESFORCE_PASSWORD' => 'salesforce-password',
'SALESFORCE_CLIENT_ID' => 'salesforce-client-id',
'SALESFORCE_CLIENT_SECRET' => 'salesforce-client-secret',
'SALESFORCE_HOST' => 'example.com'
})

allow(Restforce).to receive(:new).and_return(mock_client)
allow(mock_client).to receive(:create)
end

it 'creates a Salesforce account with correct data' do
described_class.perform_now(school.id)

expect(mock_client).to have_received(:create).with('Account', expected_account_data)
end

it 'concatenates the address fields' do
school.update(address_line_2: 'Address line 2')
expected_data = expected_account_data.merge(BillingStreet: "16711 Mulholland Drive\nAddress line 2")

described_class.perform_now(school.id)

expect(mock_client).to have_received(:create).with('Account', expected_data)
end

it 'configures Restforce client with correct credentials' do
described_class.perform_now(school.id)

expect(Restforce).to have_received(:new).with(
username: 'salesforce-username',
password: 'salesforce-password',
client_id: 'salesforce-client-id',
client_secret: 'salesforce-client-secret',
host: 'example.com',
api_version: '57.0'
)
end

it 'raises error when school is not found' do
expect { described_class.perform_now('not-an-id') }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
4 changes: 4 additions & 0 deletions spec/models/school_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@
school.rejected_at = Time.zone.now
expect { school.verify! }.to raise_error(ActiveRecord::RecordInvalid)
end

it 'enqueues SalesforceSyncJob with the school id' do
expect { school.verify! }.to have_enqueued_job(SalesforceSyncJob).with(school.id)
end
end

describe '#format_uk_postal_code' do
Expand Down