Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URN db version #311

Closed
wants to merge 1 commit into from
Closed
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
135 changes: 33 additions & 102 deletions app/models/urn.rb
Original file line number Diff line number Diff line change
@@ -1,115 +1,46 @@
# frozen_string_literal: true

# Urn represents a pseudo random Uniform Resource Name (URN) generator.
# Invoking the method `next` returns a unique URN with a fixed prefix
# and a random alphanumeric suffix.
# == Schema Information
#
# Urn.configure do |c|
# c.max_suffix = 11
# c.seeds = { teacher: ENV['TEACHER_URN_SEED'] }
# c.urns = ->(route) { Application.where(application_route: route).pluck(:urn) }
# end
# Table name: urns
#
# Example:
# id :bigint not null, primary key
# code :string
# prefix :string
# suffix :integer
# created_at :datetime not null
# updated_at :datetime not null
#
# Urn.next('teacher') # => "IRPTE12345"
# Urn.next('teacher') # => "IRPTE12345"
# Urn.next('salaried_trainee') # => "IRPST12345"
#
class Urn
class NoUrnAvailableError < StandardError; end

class Config
def initialize
@default_prefix = "IRP"
@default_max_suffix = 99_999
@default_codes = {
teacher: "TE",
salaried_trainee: "ST",
}.with_indifferent_access
@default_urns = ->(_) { [] }
end

attr_writer :prefix, :codes, :max_suffix, :seeds, :urns, :padding_size

def prefix
@prefix || @default_prefix
end

def codes
(@codes || @default_codes).with_indifferent_access
end

def max_suffix
@max_suffix || @default_max_suffix
end

def padding_size
@padding_size || max_suffix.to_s.size
end

def seeds
(@seeds || {}).with_indifferent_access
end
class Urn < ApplicationRecord
class NoUrnAvailableError < StandardError; end

def urns
@urns || @default_urns
end
PREFIX = "IRP".freeze
MAX_SUFFIX = 99_999
PADDING_SIZE = MAX_SUFFIX.to_s.size
VALID_CODES = {
"teacher" => "TE",
"salaried_trainee" => "ST",
}.freeze

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}")
end

class << self
def configure
yield(config)
end

def config
return @config if @config.present?

@config = Config.new
end

def next(route)
routes[route].next
rescue KeyError
raise(ArgumentError, "Invalid route: #{route}, must be one of #{config.codes.keys}")
end

private

def routes
@routes ||= Concurrent::Hash.new do |hash, route|
hash[route] = urn_enumerator(
config.codes.fetch(route),
config.seeds.fetch(route, Random.new_seed),
config.urns.call(route),
)
end
end

def urns(code, seed)
Array
.new(config.max_suffix) { formatter(code, _1) }
.drop(1)
.shuffle!(random: Random.new(seed))
end

def formatter(code, suffix)
[config.prefix, code, sprintf("%0#{config.padding_size}d", suffix)].join
end

def available_urns(code, seed, used_urns)
urns(code, seed) - used_urns
end

def urn_enumerator(code, seed, used_urns)
list = Concurrent::Array.new(available_urns(code, seed, used_urns))
error_msg = "you have exhausted urn for code #{code} you need to increase the size of the suffix"

Enumerator.new do |yielder|
list.each { yielder << _1 }

raise(NoUrnAvailableError, error_msg)
end
end
def to_s
[prefix, code, sprintf("%0#{PADDING_SIZE}d", suffix)].join
end
end
62 changes: 62 additions & 0 deletions app/services/generate_urns.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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
2 changes: 2 additions & 0 deletions bin/app-startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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
Expand Down
7 changes: 0 additions & 7 deletions config/initializers/urn.rb

This file was deleted.

12 changes: 12 additions & 0 deletions db/migrate/20230927092305_create_urns.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
9 changes: 9 additions & 0 deletions db/schema.rb

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

10 changes: 10 additions & 0 deletions lib/tasks/urn.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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
7 changes: 7 additions & 0 deletions spec/factories/urns.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FactoryBot.define do
factory :urn do
prefix { "IRP" }
code { %w[TE ST].sample }
sequence(:suffix) { _1 }
end
end
6 changes: 6 additions & 0 deletions spec/features/admin_console/applications_list_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@

def given_there_are_few_applications
# Create 2 specific applications for search tests
create_list(:urn, 25, code: "TE")
create_list(:urn, 25, code: "ST")
unique_applicant = create(:applicant, given_name: "Unique Given Name", middle_name: "Unique Middle Name", family_name: "Unique Family Name", email_address: "[email protected]")
create(:application, applicant: unique_applicant, urn: "Unique Urn 1")

Expand All @@ -70,12 +72,16 @@ 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
Expand Down
90 changes: 90 additions & 0 deletions spec/fixtures/urns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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

Loading