Skip to content

Commit 8f39c7c

Browse files
danhalsonCopilot
andauthored
1069-add-immediate-onboarding (#640)
## Status - Closes RaspberryPiFoundation/digital-editor-issues#1069 ## What's changed? - Separates onboarding logic from verification and adds a new service - Sets `administrative_area` and `postal_code` to required fields, to match the frontend - This is all wrapped behind a feature flag `ENABLE_IMMEDIATE_SCHOOL_ONBOARDING` - This will need enabling / disabling as required via terraform or locally to develop against this feature ## Steps to perform after deploying to production N/A --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bbb4d69 commit 8f39c7c

18 files changed

Lines changed: 488 additions & 172 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ EDITOR_ENCRYPTION_KEY=a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789a
5151
# The sandbox creds can be found in 1password under "Google Cloud Console: CEfE Sandbox"
5252
GOOGLE_CLIENT_ID=changeme.apps.googleusercontent.com
5353
GOOGLE_CLIENT_SECRET=changeme
54+
55+
# Enable immediate onboarding for schools
56+
ENABLE_IMMEDIATE_SCHOOL_ONBOARDING=true

app/controllers/api/schools_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def show
1616
end
1717

1818
def create
19-
result = School::Create.call(school_params:, creator_id: current_user.id)
19+
result = School::Create.call(school_params:, creator_id: current_user.id, token: current_user.token)
2020

2121
if result.success?
2222
@school = result[:school]

app/jobs/school_import_job.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def import_school(school_data)
7171
School.transaction do
7272
result = School::Create.call(
7373
school_params: school_params,
74-
creator_id: proposed_owner[:id]
74+
creator_id: proposed_owner[:id],
75+
token: @token
7576
)
7677

7778
if result.success?

app/models/school.rb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class School < ApplicationRecord
1515
validates :website, presence: true, format: { with: VALID_URL_REGEX, message: I18n.t('validations.school.website') }
1616
validates :address_line_1, presence: true
1717
validates :municipality, presence: true
18+
validates :administrative_area, presence: true
19+
validates :postal_code, presence: true
1820
validates :country_code, presence: true, inclusion: { in: ISO3166::Country.codes }
1921
validates :reference,
2022
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_blank: true, message: I18n.t('validations.school.reference_urn_exists') },
@@ -38,8 +40,6 @@ class School < ApplicationRecord
3840
validates :verified_at, absence: { if: proc { |school| school.rejected? } }
3941
validates :code,
4042
uniqueness: { allow_nil: true },
41-
presence: { if: proc { |school| school.verified? } },
42-
absence: { unless: proc { |school| school.verified? } },
4343
format: { with: /\d\d-\d\d-\d\d/, allow_nil: true }
4444
validate :verified_at_cannot_be_changed
4545
validate :code_cannot_be_changed
@@ -50,6 +50,9 @@ class School < ApplicationRecord
5050

5151
before_save :format_uk_postal_code, if: :should_format_uk_postal_code?
5252

53+
# TODO: Remove the conditional once the feature flag is retired
54+
after_create :generate_code!, if: -> { FeatureFlags.immediate_school_onboarding? }
55+
5356
def self.find_for_user!(user)
5457
school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id)
5558
raise ActiveRecord::RecordNotFound unless school
@@ -70,9 +73,19 @@ def rejected?
7073
end
7174

7275
def verify!
76+
# TODO: Remove this line once the feature flag is retired
77+
generate_code! unless FeatureFlags.immediate_school_onboarding?
78+
79+
update!(verified_at: Time.zone.now)
80+
end
81+
82+
def generate_code!
83+
return code if code.present?
84+
7385
attempts = 0
7486
begin
75-
update!(verified_at: Time.zone.now, code: ForEducationCodeGenerator.generate)
87+
new_code = ForEducationCodeGenerator.generate
88+
update!(code: new_code)
7689
rescue ActiveRecord::RecordInvalid => e
7790
raise unless e.record.errors[:code].include?('has already been taken') && attempts <= 5
7891

@@ -86,6 +99,8 @@ def reject
8699
end
87100

88101
def reopen
102+
return false unless rejected?
103+
89104
update(rejected_at: nil)
90105
end
91106

@@ -130,7 +145,7 @@ def rejected_at_cannot_be_changed
130145
end
131146

132147
def code_cannot_be_changed
133-
errors.add(:code, 'cannot be changed after verification') if code_was.present? && code_changed?
148+
errors.add(:code, 'cannot be changed after onboarding') if code_was.present? && code_changed?
134149
end
135150

136151
def should_format_uk_postal_code?
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class SchoolOnboardingService
4+
attr_reader :school
5+
6+
def initialize(school)
7+
@school = school
8+
end
9+
10+
def onboard(token:)
11+
School.transaction do
12+
Role.owner.create!(user_id: school.creator_id, school:)
13+
Role.teacher.create!(user_id: school.creator_id, school:)
14+
15+
ProfileApiClient.create_school(token:, id: school.id, code: school.code)
16+
end
17+
rescue StandardError => e
18+
Sentry.capture_exception(e)
19+
Rails.logger.error { "Failed to onboard school #{@school.id}: #{e.message}" }
20+
false
21+
else
22+
true
23+
end
24+
end

app/services/school_verification_service.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ def initialize(school)
77
@school = school
88
end
99

10-
def verify(token:)
10+
def verify(token: nil)
11+
success = false
1112
School.transaction do
1213
school.verify!
13-
Role.owner.create!(user_id: school.creator_id, school:)
14-
Role.teacher.create!(user_id: school.creator_id, school:)
15-
ProfileApiClient.create_school(token:, id: school.id, code: school.code)
14+
15+
# TODO: Remove this line, once the feature flag is retired
16+
success = FeatureFlags.immediate_school_onboarding? || SchoolOnboardingService.new(school).onboard(token: token)
17+
18+
# TODO: Remove this line, once the feature flag is retired
19+
raise ActiveRecord::Rollback unless success
1620
end
1721
rescue StandardError => e
1822
Sentry.capture_exception(e)
1923
Rails.logger.error { "Failed to verify school #{@school.id}: #{e.message}" }
2024
false
2125
else
22-
true
26+
# TODO: Return 'true', once the feature flag is retired
27+
success
2328
end
2429

2530
delegate :reject, to: :school

lib/concepts/school/operations/create.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@
33
class School
44
class Create
55
class << self
6-
def call(school_params:, creator_id:)
6+
def call(school_params:, creator_id:, token:)
77
response = OperationResponse.new
88
response[:school] = build_school(school_params.merge!(creator_id:))
9-
response[:school].save!
9+
10+
School.transaction do
11+
response[:school].save!
12+
13+
# TODO: Remove this conditional once the feature flag is retired
14+
if FeatureFlags.immediate_school_onboarding?
15+
onboarded = SchoolOnboardingService.new(response[:school]).onboard(token:)
16+
raise 'School onboarding failed' unless onboarded
17+
end
18+
end
19+
1020
response
1121
rescue StandardError => e
1222
Sentry.capture_exception(e)
13-
response[:error] = response[:school].errors
23+
response[:error] = response[:school].errors.presence || [e.message]
1424
response[:error_types] = response[:school].errors.details
1525

1626
response

lib/feature_flags.rb

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+
module FeatureFlags
4+
def self.immediate_school_onboarding?
5+
ENV['ENABLE_IMMEDIATE_SCHOOL_ONBOARDING'] == 'true'
6+
end
7+
end

lib/tasks/seeds_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def create_school(creator_id, school_id = nil)
2020
school.name = Faker::Educator.secondary_school
2121
school.website = Faker::Internet.url(scheme: 'https')
2222
school.address_line_1 = Faker::Address.street_address
23+
school.administrative_area = "#{Faker::Address.city}shire"
2324
school.municipality = Faker::Address.city
25+
school.postal_code = Faker::Address.postcode
2426
school.country_code = country_code
2527
school.creator_id = creator_id
2628
school.creator_agree_authority = true

spec/concepts/school/create_spec.rb

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
name: 'Test School',
99
website: 'http://www.example.com',
1010
address_line_1: 'Address Line 1',
11+
administrative_area: 'Greater London',
1112
municipality: 'Greater London',
13+
postal_code: 'SW1A 1AA',
1214
country_code: 'GB',
1315
reference: '100000',
1416
creator_agree_authority: true,
@@ -21,27 +23,31 @@
2123
let(:token) { UserProfileMock::TOKEN }
2224
let(:creator_id) { SecureRandom.uuid }
2325

26+
before do
27+
allow(ProfileApiClient).to receive(:create_school).and_return(true)
28+
end
29+
2430
it 'returns a successful operation response' do
25-
response = described_class.call(school_params:, creator_id:)
31+
response = described_class.call(school_params:, creator_id:, token:)
2632
expect(response.success?).to be(true)
2733
end
2834

2935
it 'creates a school' do
30-
expect { described_class.call(school_params:, creator_id:) }.to change(School, :count).by(1)
36+
expect { described_class.call(school_params:, creator_id:, token:) }.to change(School, :count).by(1)
3137
end
3238

3339
it 'returns the school in the operation response' do
34-
response = described_class.call(school_params:, creator_id:)
40+
response = described_class.call(school_params:, creator_id:, token:)
3541
expect(response[:school]).to be_a(School)
3642
end
3743

3844
it 'assigns the name' do
39-
response = described_class.call(school_params:, creator_id:)
45+
response = described_class.call(school_params:, creator_id:, token:)
4046
expect(response[:school].name).to eq('Test School')
4147
end
4248

4349
it 'assigns the creator_id' do
44-
response = described_class.call(school_params:, creator_id:)
50+
response = described_class.call(school_params:, creator_id:, token:)
4551
expect(response[:school].creator_id).to eq(creator_id)
4652
end
4753

@@ -53,27 +59,67 @@
5359
end
5460

5561
it 'does not create a school' do
56-
expect { described_class.call(school_params:, creator_id:) }.not_to change(School, :count)
62+
expect { described_class.call(school_params:, creator_id:, token:) }.not_to change(School, :count)
5763
end
5864

5965
it 'returns a failed operation response' do
60-
response = described_class.call(school_params:, creator_id:)
66+
response = described_class.call(school_params:, creator_id:, token:)
6167
expect(response.failure?).to be(true)
6268
end
6369

6470
it 'returns the correct number of objects in the operation response' do
65-
response = described_class.call(school_params:, creator_id:)
66-
expect(response[:error].count).to eq(9)
71+
response = described_class.call(school_params:, creator_id:, token:)
72+
expect(response[:error].count).to eq(11)
6773
end
6874

6975
it 'returns the correct type of object in the operation response' do
70-
response = described_class.call(school_params:, creator_id:)
76+
response = described_class.call(school_params:, creator_id:, token:)
7177
expect(response[:error].first).to be_a(ActiveModel::Error)
7278
end
7379

7480
it 'sent the exception to Sentry' do
75-
described_class.call(school_params:, creator_id:)
81+
described_class.call(school_params:, creator_id:, token:)
7682
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
7783
end
7884
end
85+
86+
describe 'when immediate onboarding is enabled' do
87+
# TODO: Remove this block once the feature flag is retired
88+
around do |example|
89+
ClimateControl.modify(ENABLE_IMMEDIATE_SCHOOL_ONBOARDING: 'true') do
90+
example.run
91+
end
92+
end
93+
94+
let(:onboarding_service) { instance_spy(SchoolOnboardingService, onboard: true) }
95+
96+
before do
97+
allow(SchoolOnboardingService).to receive(:new).and_return(onboarding_service)
98+
end
99+
100+
it 'calls the onboarding service' do
101+
described_class.call(school_params:, creator_id:, token:)
102+
expect(onboarding_service).to have_received(:onboard).with(token:)
103+
end
104+
end
105+
106+
# TODO: Remove these examples once the feature flag is retired
107+
describe 'when immediate onboarding is disabled' do
108+
around do |example|
109+
ClimateControl.modify(ENABLE_IMMEDIATE_SCHOOL_ONBOARDING: nil) do
110+
example.run
111+
end
112+
end
113+
114+
let(:onboarding_service) { instance_spy(SchoolOnboardingService) }
115+
116+
before do
117+
allow(SchoolOnboardingService).to receive(:new).and_return(onboarding_service)
118+
end
119+
120+
it 'does not call the onboarding service' do
121+
described_class.call(school_params:, creator_id:, token:)
122+
expect(onboarding_service).not_to have_received(:onboard)
123+
end
124+
end
79125
end

0 commit comments

Comments
 (0)