Skip to content

Commit 3209c41

Browse files
committed
Implement School sync to Salesforce
1 parent 54e90b2 commit 3209c41

7 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module Salesforce
4+
class SchoolSyncJob < SalesforceSyncJob
5+
MODEL_CLASS = Salesforce::School
6+
7+
FIELD_MAPPINGS = {
8+
editoruuid__c: :id,
9+
name: :name,
10+
editorreference__c: :reference,
11+
addressline1__c: :address_line_1,
12+
addressline2__c: :address_line_2,
13+
editormunicipality__c: :municipality,
14+
editoradministrativearea__c: :administrative_area,
15+
postcode__c: :postal_code,
16+
countrycode__c: :country_code,
17+
verifiedat__c: :verified_at,
18+
createdat__c: :created_at,
19+
updatedat__c: :updated_at,
20+
rejectedat__c: :rejected_at,
21+
website__c: :website,
22+
userorigin__c: :user_origin,
23+
districtnamesupplied__c: :district_name,
24+
ncesid__c: :district_nces_id,
25+
schoolrollnumber__c: :school_roll_number
26+
}.freeze
27+
28+
def perform(school_id:)
29+
school = ::School.find(school_id)
30+
31+
sf_school = Salesforce::School.find_or_initialize_by(editoruuid__c: school_id)
32+
sf_school.attributes = sf_school_attributes(school:)
33+
sf_school.save!
34+
end
35+
36+
private
37+
38+
def sf_school_attributes(school:)
39+
mapped_attributes(school:).to_h do |sf_field, value|
40+
value = 'for_education' if sf_field == :userorigin__c && value.nil?
41+
value = truncate_value(sf_field:, value:) if value.is_a?(String)
42+
43+
[sf_field, value]
44+
end
45+
end
46+
47+
def mapped_attributes(school:)
48+
FIELD_MAPPINGS.transform_values do |school_field|
49+
school.send(school_field)
50+
end
51+
end
52+
end
53+
end

app/models/salesforce/school.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module Salesforce
4+
class School < Salesforce::Base
5+
self.table_name = 'salesforce.editor__c'
6+
self.primary_key = :editoruuid__c
7+
end
8+
end

app/models/school.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class School < ApplicationRecord
5252
# TODO: Remove the conditional once the feature flag is retired
5353
after_create :generate_code!, if: -> { FeatureFlags.immediate_school_onboarding? }
5454

55+
after_commit :do_salesforce_sync, on: %i[create update]
56+
5557
def self.find_for_user!(user)
5658
school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id)
5759
raise ActiveRecord::RecordNotFound unless school
@@ -169,4 +171,11 @@ def format_uk_postal_code
169171
# ensures UK postcodes are always formatted correctly (as the inward code is always 3 chars long)
170172
self.postal_code = "#{cleaned_postal_code[0..-4]} #{cleaned_postal_code[-3..]}"
171173
end
174+
175+
def do_salesforce_sync
176+
return unless ENV.fetch('SALESFORCE_ENABLED', 'true') == 'true'
177+
178+
Salesforce::SchoolSyncJob.perform_later(school_id: id)
179+
Salesforce::ContactSyncJob.perform_later(school_id: id)
180+
end
172181
end

lib/tasks/salesforce_sync.rake

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
namespace :salesforce_sync do
4+
desc 'Sync all Schools to Salesforce'
5+
task school: :environment do
6+
School.pluck(:id).each do |school_id|
7+
Salesforce::SchoolSyncJob.perform_later(school_id:)
8+
end
9+
end
10+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
FactoryBot.define do
4+
factory(:salesforce_school, class: 'Salesforce::School') do
5+
editoruuid__c { SecureRandom.uuid }
6+
end
7+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Salesforce::SchoolSyncJob do
6+
subject(:perform_job) { described_class.perform_now(school_id: school.id) }
7+
8+
let(:school) { create(:school) }
9+
10+
context 'when the job has run' do
11+
before { perform_job }
12+
13+
it 'syncs all FIELD_MAPPINGS to the correct school values' do
14+
sf_school = Salesforce::School.find_by(editoruuid__c: school.id)
15+
described_class::FIELD_MAPPINGS.each do |sf_field, school_field|
16+
expected = Salesforce::School.type_for_attribute(sf_field).cast(school.send(school_field))
17+
expect(sf_school.send(sf_field)).to eq(expected),
18+
"Expected #{sf_field} to equal school.#{school_field}"
19+
end
20+
end
21+
22+
context 'when an address field is very long' do
23+
let(:school) { create(:school, address_line_1: '❌' * 300) }
24+
25+
it 'truncates addressline1__c' do
26+
sf_school = Salesforce::School.find_by(editoruuid__c: school.id)
27+
expect(sf_school.addressline1__c).to end_with('…')
28+
expect(sf_school.addressline1__c.length).to be < school.address_line_1.length
29+
end
30+
end
31+
32+
context 'when the school is verified' do
33+
let(:school) { create(:verified_school) }
34+
35+
it 'syncs verifiedat__c' do
36+
sf_school = Salesforce::School.find_by(editoruuid__c: school.id)
37+
expect(sf_school.verifiedat__c).to eq(school.verified_at)
38+
end
39+
end
40+
41+
context 'when the school is rejected' do
42+
let(:school) { create(:school, rejected_at: Time.current) }
43+
44+
it 'syncs rejectedat__c' do
45+
sf_school = Salesforce::School.find_by(editoruuid__c: school.id)
46+
expect(sf_school.rejectedat__c).to eq(school.rejected_at)
47+
end
48+
end
49+
end
50+
51+
context 'when the Salesforce school fails to save' do
52+
let(:sf_school) { instance_double(Salesforce::School) }
53+
54+
before do
55+
allow(Salesforce::School).to receive(:find_or_initialize_by).with(editoruuid__c: school.id).and_return(sf_school)
56+
allow(sf_school).to receive(:attributes=)
57+
allow(sf_school).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
58+
end
59+
60+
it 'raises an error' do
61+
expect { perform_job }.to raise_error ActiveRecord::RecordInvalid
62+
end
63+
end
64+
65+
context 'when SALESFORCE_ENABLED is false' do
66+
around do |example|
67+
ClimateControl.modify(SALESFORCE_ENABLED: 'false') do
68+
example.run
69+
end
70+
end
71+
72+
it 'discards the job without syncing' do
73+
perform_job
74+
expect(Salesforce::School.find_by(editoruuid__c: school.id)).to be_nil
75+
end
76+
end
77+
end

spec/models/school_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,40 @@
632632
end
633633
end
634634

635+
describe 'salesforce sync' do
636+
it 'enqueues Salesforce::SchoolSyncJob on create' do
637+
expect { create(:school) }.to have_enqueued_job(Salesforce::SchoolSyncJob)
638+
end
639+
640+
it 'enqueues Salesforce::ContactSyncJob on create' do
641+
expect { create(:school) }.to have_enqueued_job(Salesforce::ContactSyncJob)
642+
end
643+
644+
it 'enqueues Salesforce::SchoolSyncJob on update' do
645+
school = create(:school)
646+
expect { school.update!(name: 'Updated Name') }.to have_enqueued_job(Salesforce::SchoolSyncJob)
647+
end
648+
649+
it 'enqueues Salesforce::ContactSyncJob on update' do
650+
school = create(:school)
651+
expect { school.update!(name: 'Updated Name') }.to have_enqueued_job(Salesforce::ContactSyncJob)
652+
end
653+
654+
context 'when SALESFORCE_ENABLED is false' do
655+
around do |example|
656+
ClimateControl.modify(SALESFORCE_ENABLED: 'false') { example.run }
657+
end
658+
659+
it 'does not enqueue Salesforce::SchoolSyncJob on create' do
660+
expect { create(:school) }.not_to have_enqueued_job(Salesforce::SchoolSyncJob)
661+
end
662+
663+
it 'does not enqueue Salesforce::ContactSyncJob on create' do
664+
expect { create(:school) }.not_to have_enqueued_job(Salesforce::ContactSyncJob)
665+
end
666+
end
667+
end
668+
635669
describe '#reopen' do
636670
it 'sets rejected_at to nil' do
637671
school.reject

0 commit comments

Comments
 (0)