Skip to content

Commit

Permalink
Add uniqueness constraint on applications.urn
Browse files Browse the repository at this point in the history
Urn.next_available method returns a unique urn based the existing ones
for the application route.

Over time finding a unique unused urn will be a longer operation
  • Loading branch information
fumimowdan committed Sep 20, 2023
1 parent 9a5a754 commit 7d7b7d5
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 14 deletions.
1 change: 1 addition & 0 deletions app/models/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ def mark_as_qa!
validates(:subject, presence: true)
validates(:visa_type, presence: true)
validates(:applicant, presence: true)
validates(:urn, presence: true, uniqueness: true)
end
31 changes: 26 additions & 5 deletions app/models/urn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,40 @@
# Urn.generate('salaried_trainee') # => "IRPST12345"
#
class Urn
class GenerationError < StandardError; end

CHARSET = %w[0 1 2 3 4 5 6 7 8 9].freeze
PREFIX = "IRP"
LENGTH = 5

MAX_URN_SET_PER_ROUTE = ("9" * LENGTH).to_i
INVALID_SUFFIX = "0" * LENGTH

private_constant :CHARSET, :PREFIX, :LENGTH

attr_reader :value
attr_writer :suffix

def self.next_available(route, used_urns)
raise(GenerationError, "No more urn available for route #{route}") if used_urns.size >= MAX_URN_SET_PER_ROUTE

urn = nil

loop do
urn = generate(route)

next if urn.include?(INVALID_SUFFIX)
break unless used_urns.include?(urn)
end

urn
end

def self.generate(applicant_type)
code = applicant_type_code(applicant_type)
PREFIX + code + Array.new(LENGTH) { CHARSET.sample }.join
end

CHARSET = %w[0 1 2 3 4 5 6 7 8 9].freeze
PREFIX = "IRP"
LENGTH = 5
private_constant :CHARSET, :PREFIX, :LENGTH

def self.applicant_type_code(applicant_type)
case applicant_type
when "teacher"
Expand Down
22 changes: 17 additions & 5 deletions app/services/submit_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ def initialize(form, ip_address)
@form = form
@ip_address = ip_address
@success = false
@counter = 0
end
attr_reader :form, :ip_address, :application
attr_reader :form, :ip_address, :application, :counter

delegate :errors, to: :form

Expand All @@ -30,6 +31,9 @@ def submit_form!
create_application_records
send_applicant_email
@success = true
rescue StandardError => e
Sentry.capture_exception(e)
errors.add(:base, :technical_error, message: "We are unable to submit your application at the moment. Please try again later")
end

private
Expand Down Expand Up @@ -75,20 +79,20 @@ def create_applicant
city: form.city,
postcode: form.postcode,
},
school: @school,
school: school,
)
end

def create_application
@application = Application.create!(
applicant: @applicant,
applicant: applicant,
application_date: Date.current.to_s,
application_route: form.application_route,
application_progress: ApplicationProgress.new,
date_of_entry: form.date_of_entry,
start_date: form.start_date,
subject: SubjectStep.new(form).answer.formatted_value,
urn: Urn.generate(form.application_route),
urn: Urn.next_available(form.application_route, used_urns),
visa_type: form.visa_type,
)
end
Expand All @@ -100,10 +104,18 @@ def delete_form
def send_applicant_email
GovukNotifyMailer
.with(
email: @applicant.email_address,
email: applicant.email_address,
urn: application.urn,
)
.application_submission
.deliver_later
end

def used_urns
Application
.where(application_route: form.application_route)
.pluck(:urn)
end

attr_reader :school, :applicant
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddUniquenessToApplicationUrn < ActiveRecord::Migration[7.0]
def up
drop_view :duplicate_applications
add_index(:applications, :urn)
create_view :duplicate_applications, version: 3
end

def down; end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions spec/models/application_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
it { expect(application).to validate_presence_of(:subject) }
it { expect(application).to validate_presence_of(:visa_type) }
it { expect(application).to validate_presence_of(:applicant) }
it { expect(application).to validate_presence_of(:urn) }
it { expect(application).to validate_uniqueness_of(:urn) }
end
end

Expand Down
34 changes: 32 additions & 2 deletions spec/models/urn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
require "rails_helper"

RSpec.describe Urn do
subject(:urn) { described_class.generate(applicant_type) }

describe ".generate" do
subject(:urn) { described_class.generate(applicant_type) }

context 'when applicant type is "teacher"' do
let(:applicant_type) { "teacher" }

Expand Down Expand Up @@ -36,4 +36,34 @@
end
end
end

describe ".next_available" do
subject(:urn) { described_class.next_available(application_route, used_urns) }

let(:application_route) { "teacher" }
let(:used_urns) { %w[IRPTE45632 IRPTE74125] }

context "when all urns possible have been used" do
let(:used_urns) { Array.new(Urn::MAX_URN_SET_PER_ROUTE) }

it "raises an GenerationError" do
expect { urn }.to raise_error(Urn::GenerationError, "No more urn available for route #{application_route}")
end
end

context "never returns an urn with only 0 in suffix" do
before do
allow(described_class).to receive(:generate).and_return(invalid_urn, valid_urn)
end

let(:invalid_urn) { "IRPTE00000" }
let(:valid_urn) { "IRPTE22222" }

it { expect(urn).to eq(valid_urn) }
end

context "returns an unused urn" do
it { expect(urn).to include("IRPTE") }
end
end
end
2 changes: 1 addition & 1 deletion spec/services/submit_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@

context "applicant email" do
before do
allow(Urn).to receive(:generate).and_return(urn)
allow(Urn).to receive(:next_available).and_return(urn)
end

let(:urn) { "SOMEURN" }
Expand Down

0 comments on commit 7d7b7d5

Please sign in to comment.