diff --git a/app/models/urn.rb b/app/models/urn.rb index e88c85e1..56330c1c 100644 --- a/app/models/urn.rb +++ b/app/models/urn.rb @@ -1,41 +1,38 @@ -# == Schema Information +# frozen_string_literal: true + +# Urn represents a Uniform Resource Name (URN) generator. +# It generates a URN with a fixed prefix and a random alphanumeric suffix. # -# Table name: urns # -# id :bigint not null, primary key -# code :string -# prefix :string -# suffix :integer -# created_at :datetime not null -# updated_at :datetime not null +# Example: # -class Urn < ApplicationRecord - class NoUrnAvailableError < StandardError; end - - PREFIX = "IRP".freeze - MAX_SUFFIX = 99_999 - PADDING_SIZE = MAX_SUFFIX.to_s.size - VALID_CODES = { - "teacher" => "TE", - "salaried_trainee" => "ST", - }.freeze +# Urn.generate('teacher') # => "IRPTE12345" +# Urn.generate('teacher') # => "IRPTE12345" +# Urn.generate('salaried_trainee') # => "IRPST12345" +# +class Urn + attr_reader :value + attr_writer :suffix - def self.next(route) - code = VALID_CODES.fetch(route) - Urn.transaction do - urn = find_by!(code:) - urn.destroy! - urn.to_s - end - rescue KeyError => e - Sentry.capture_exception(e) - raise(ArgumentError, "Unknown route #{route}") - rescue ActiveRecord::RecordNotFound => e - Sentry.capture_exception(e) - raise(NoUrnAvailableError, "There no more unique URN available for #{route}") + def self.generate(applicant_type) + code = applicant_type_code(applicant_type) + PREFIX + code + Array.new(LENGTH) { CHARSET.sample }.join end - def to_s - [prefix, code, sprintf("%0#{PADDING_SIZE}d", suffix)].join + 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" + "TE" + when "salaried_trainee" + "ST" + else + raise(ArgumentError, "Invalid applicant type: #{applicant_type}") + end end + private_methods :applicant_type_code end diff --git a/app/services/generate_urns.rb b/app/services/generate_urns.rb deleted file mode 100644 index a0a33274..00000000 --- a/app/services/generate_urns.rb +++ /dev/null @@ -1,62 +0,0 @@ -# Service responsible for the generation of all urns -# It will save the set of available urns based the current URN format -# and store it in the database URNs table. -# -# The Urn model will then be able to fetch the next available unique and -# random urn for application submition -# -# Example: -# -# Urn.next("teacher") # => "IRPTE12345" -# Urn.next("teacher") # => "IRPTE12345" -# Urn.next("salaried_trainee") # => "IRPST12345" -# -class GenerateUrns - def self.call - return if Urn.count.positive? # Do not override the current urn state - - Urn.transaction do - Urn::VALID_CODES.each_value do |code| - new(code:).generate - end - end - end - - def initialize(code:) - @code = code - end - - attr_reader :code - - def generate - data = unused_urns.map do |suffix| - { prefix: Urn::PREFIX, code: code, suffix: suffix } - end - Urn.insert_all(data) # rubocop:disable Rails/SkipsModelValidations - end - -private - - def unused_urns - generate_suffixes - existing_suffixes - end - - def generate_suffixes - Array - .new(Urn::MAX_SUFFIX) { _1 } - .drop(1) - .shuffle! - end - - def existing_suffixes - route = Urn::VALID_CODES.key(code) - Application - .where(application_route: route) - .pluck(:urn) - .map { extract_suffix(_1) } - end - - def extract_suffix(urn) - urn.match(/\d+/)[0].to_i - end -end diff --git a/app/services/submit_form.rb b/app/services/submit_form.rb index a6ccb4d5..28bdbaff 100644 --- a/app/services/submit_form.rb +++ b/app/services/submit_form.rb @@ -88,7 +88,7 @@ def create_application date_of_entry: form.date_of_entry, start_date: form.start_date, subject: SubjectStep.new(form).answer.formatted_value, - urn: Urn.next(form.application_route), + urn: Urn.generate(form.application_route), visa_type: form.visa_type, ) end diff --git a/bin/app-startup.sh b/bin/app-startup.sh index 9da89774..0df4c416 100755 --- a/bin/app-startup.sh +++ b/bin/app-startup.sh @@ -7,8 +7,6 @@ set -e # run migrations bundle exec rails db:migrate -# Front load urn generation -bundle exec rake urn:generate # add seed data in review environment if [[ "$RAILS_ENV" = "review" || "$RAILS_ENV" = "development" ]]; then diff --git a/db/migrate/20230927092305_create_urns.rb b/db/migrate/20230927092305_create_urns.rb deleted file mode 100644 index e13281c1..00000000 --- a/db/migrate/20230927092305_create_urns.rb +++ /dev/null @@ -1,12 +0,0 @@ -class CreateUrns < ActiveRecord::Migration[7.0] - def change - create_table :urns do |t| - t.string :prefix - t.string :code - t.integer :suffix - - t.timestamps - end - add_index :urns, :code - end -end diff --git a/db/migrate/20231009110217_add_application_indexes.rb b/db/migrate/20231009110217_add_application_indexes.rb new file mode 100644 index 00000000..a1d241ad --- /dev/null +++ b/db/migrate/20231009110217_add_application_indexes.rb @@ -0,0 +1,7 @@ +class AddApplicationIndexes < ActiveRecord::Migration[7.0] + def change + drop_table :urns + add_index :applications, :urn, unique: true + add_index :applications, :application_route + end +end diff --git a/db/schema.rb b/db/schema.rb index 4d5e00a1..18a49063 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_02_115920) do +ActiveRecord::Schema[7.0].define(version: 2023_10_09_110217) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "plpgsql" @@ -84,6 +84,8 @@ t.datetime "standing_data_csv_downloaded_at" t.datetime "payroll_csv_downloaded_at" t.index ["applicant_id"], name: "index_applications_on_applicant_id" + t.index ["application_route"], name: "index_applications_on_application_route" + t.index ["urn"], name: "index_applications_on_urn", unique: true end create_table "audits", force: :cascade do |t| @@ -183,15 +185,6 @@ t.datetime "updated_at", null: false end - create_table "urns", force: :cascade do |t| - t.string "prefix" - t.string "code" - t.integer "suffix" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["code"], name: "index_urns_on_code" - end - create_table "users", force: :cascade do |t| t.citext "email" t.datetime "created_at", null: false diff --git a/lib/tasks/urn.rake b/lib/tasks/urn.rake deleted file mode 100644 index 64c42ccd..00000000 --- a/lib/tasks/urn.rake +++ /dev/null @@ -1,10 +0,0 @@ -namespace :urn do - desc "generate and randomize unique urns" - task generate: :environment do - puts "running rake task urn:generate ..." - a = Urn.count - GenerateUrns.call - b = Urn.count - puts "#{b - a} URN created" - end -end diff --git a/spec/factories/applications.rb b/spec/factories/applications.rb index 712bf90b..49d86172 100644 --- a/spec/factories/applications.rb +++ b/spec/factories/applications.rb @@ -31,7 +31,7 @@ visa_type { VisaStep::VALID_ANSWERS_OPTIONS.reject { _1 == "Other" }.sample } date_of_entry { Time.zone.today } start_date { 1.month.from_now.to_date } - urn { Urn.next(application_route) } + urn { Urn.generate(application_route) } factory :teacher_application do application_route { "teacher" } diff --git a/spec/factories/urns.rb b/spec/factories/urns.rb deleted file mode 100644 index 5ecebff0..00000000 --- a/spec/factories/urns.rb +++ /dev/null @@ -1,18 +0,0 @@ -# == Schema Information -# -# Table name: urns -# -# id :bigint not null, primary key -# code :string -# prefix :string -# suffix :integer -# created_at :datetime not null -# updated_at :datetime not null -# -FactoryBot.define do - factory :urn do - prefix { "IRP" } - code { %w[TE ST].sample } - suffix { Array.new(10) { _1 }.sample } - end -end diff --git a/spec/features/admin_console/applications_list_spec.rb b/spec/features/admin_console/applications_list_spec.rb index 8cb032cd..85a51dee 100644 --- a/spec/features/admin_console/applications_list_spec.rb +++ b/spec/features/admin_console/applications_list_spec.rb @@ -59,8 +59,6 @@ def given_there_are_few_applications # Create 2 specific applications for search tests - create_list(:urn, 5, code: "TE") - create_list(:urn, 5, code: "ST") unique_applicant = create(:applicant, given_name: "Unique Given Name", middle_name: "Unique Middle Name", family_name: "Unique Family Name", email_address: "unique@example.com") create(:application, applicant: unique_applicant, urn: "Unique Urn 1") @@ -72,16 +70,12 @@ def given_there_are_few_applications end def given_there_is_an_application_that_breached_sla - create_list(:urn, 5, code: "TE") - create_list(:urn, 5, code: "ST") applicant = create(:applicant) application = create(:application, applicant:) application.application_progress.update(initial_checks_completed_at: 4.days.ago) end def given_there_are_applications_with_different_dates - create_list(:urn, 5, code: "TE") - create_list(:urn, 5, code: "ST") create(:application, application_progress: build(:application_progress, :initial_checks_completed, status: :initial_checks)) create(:application, application_progress: build(:application_progress, :home_office_checks_completed, status: :home_office_checks)) end diff --git a/spec/fixtures/urns.yml b/spec/fixtures/urns.yml deleted file mode 100644 index e2cfb088..00000000 --- a/spec/fixtures/urns.yml +++ /dev/null @@ -1,101 +0,0 @@ -# == Schema Information -# -# Table name: urns -# -# id :bigint not null, primary key -# code :string -# prefix :string -# suffix :integer -# created_at :datetime not null -# updated_at :datetime not null -# -te_one: - suffix: 5668 - prefix: IRP - code: TE - -te_two: - suffix: 21368 - prefix: IRP - code: TE - -te_three: - suffix: 5 - prefix: IRP - code: TE - -te_four: - suffix: 76998 - prefix: IRP - code: TE - -te_five: - suffix: 6559 - prefix: IRP - code: TE - -te_six: - suffix: 6 - prefix: IRP - code: TE - -te_seven: - suffix: 2298 - prefix: IRP - code: TE - -te_eight: - suffix: 1159 - prefix: IRP - code: TE - -te_nine: - suffix: 79298 - prefix: IRP - code: TE - -te_ten: - suffix: 19549 - prefix: IRP - code: TE - -st_one: - suffix: 5668 - prefix: IRP - code: ST - -st_two: - suffix: 29968 - prefix: IRP - code: ST - -st_three: - suffix: 5 - prefix: IRP - code: ST - -st_four: - suffix: 76998 - prefix: IRP - code: ST - -st_five: - suffix: 6559 - prefix: IRP - code: ST - -st_six: - suffix: 6 - prefix: IRP - code: ST - -st_seven: - suffix: 28 - prefix: IRP - code: ST - -st_eight: - suffix: 159 - prefix: IRP - code: ST - diff --git a/spec/models/urn_spec.rb b/spec/models/urn_spec.rb index eb150367..4df75ded 100644 --- a/spec/models/urn_spec.rb +++ b/spec/models/urn_spec.rb @@ -1,60 +1,39 @@ -# == Schema Information -# -# Table name: urns -# -# id :bigint not null, primary key -# code :string -# prefix :string -# suffix :integer -# created_at :datetime not null -# updated_at :datetime not null -# +# frozen_string_literal: true + require "rails_helper" RSpec.describe Urn do - describe "next" do - subject(:next_urn) { described_class.next(route) } - - context "for application route teacher" do - let(:route) { "teacher" } - - before { create(:urn, code: "TE") } + subject(:urn) { described_class.generate(applicant_type) } - it { expect(next_urn).to match(/IRPTE\d{5}/) } - end + describe ".generate" do + context 'when applicant type is "teacher"' do + let(:applicant_type) { "teacher" } - context "for application route salaried_trainee" do - let(:route) { "salaried_trainee" } + it "generates a URN with the correct prefix and suffix" do + expect(urn).to match(/^IRPTE[0-9]{5}$/) + end - before { create(:urn, code: "ST") } + it "generates a Urn with a suffix of only characters in the CHARSET" do + charset = %w[0 1 2 3 4 5 6 7 8 9] - it { expect(next_urn).to match(/IRPST\d{5}/) } + expect(urn[5..9].chars).to all(be_in(charset)) + end end - context "when bad application route" do - let(:route) { "badroute" } + context 'when applicant type is "salaried_trainee"' do + let(:applicant_type) { "salaried_trainee" } - it { expect { next_urn }.to raise_error(ArgumentError) } + it "generates a URN with the correct prefix and suffix" do + expect(urn).to match(/^IRPST[0-9]{5}$/) + end end - context "when there is no more urn available to assign" do - let(:route) { "salaried_trainee" } + context "when an invalid applicant type is provided" do + let(:applicant_type) { "invalid_type" } - before do - allow(described_class).to receive(:find_by!).and_raise(ActiveRecord::RecordNotFound) + it "raises an ArgumentError" do + expect { urn }.to raise_error(ArgumentError, "Invalid applicant type: invalid_type") end - - it { expect { next_urn }.to raise_error(Urn::NoUrnAvailableError) } end end - - describe ".to_s" do - subject(:urn) { described_class.new(prefix:, code:, suffix:) } - - let(:prefix) { "AST" } - let(:code) { "FF" } - let(:suffix) { 65 } - - it { expect(urn.to_s).to eq("ASTFF00065") } - end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index edfe2b50..6ba490c8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -42,8 +42,7 @@ end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = Rails.root.join("spec/fixtures") - config.global_fixtures = :urns + config.fixture_path = Rails.root.join("/spec/fixtures") # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false diff --git a/spec/services/generate_urns_spec.rb b/spec/services/generate_urns_spec.rb deleted file mode 100644 index cbe58507..00000000 --- a/spec/services/generate_urns_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "rails_helper" - -RSpec.describe GenerateUrns do - describe ".generate" do - subject(:generate) { described_class.new(code:).generate } - - before do - allow(Urn).to receive(:insert_all) - stub_const "Urn::MAX_SUFFIX", 3 - generate - end - - let(:code) { "TE" } - let(:expected_data) do - [ - { prefix: "IRP", code: code, suffix: 1 }, - { prefix: "IRP", code: code, suffix: 2 }, - ] - end - - it { expect(Urn).to have_received(:insert_all).with(match_array(expected_data)) } - end -end diff --git a/spec/services/submit_form_spec.rb b/spec/services/submit_form_spec.rb index c7faac40..8606e43f 100644 --- a/spec/services/submit_form_spec.rb +++ b/spec/services/submit_form_spec.rb @@ -136,7 +136,7 @@ context "applicant email" do before do - allow(Urn).to receive(:next).and_return(urn) + allow(Urn).to receive(:generate).and_return(urn) end let(:urn) { "SOMEURN" }