Skip to content

Commit

Permalink
Create Urn model
Browse files Browse the repository at this point in the history
This will serve to render a urn and hold the current state of
available urns ready to be used.

Add GenerateUrns service

This service will create all the available urns per application route.
They will be randomized, unique and the next available urn to be taken
will retreived with the Urn.next method.

Once all the urn are exhausted this method will raise an error and the
format of the URN will need updating ie increase the size of the
suffix set.  Urn::MAX_SUFFIX

Add rake task urn:generate

This task is going to be used to ensure that when start the
application service the URNs are ready to be picked.

Update code to used `Urn.next`

* startup.sh

* submitform service

* factories

Fix specs for urn
  • Loading branch information
fumimowdan committed Oct 24, 2023
1 parent 18fb279 commit 5f40c1f
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 132 deletions.
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

0 comments on commit 5f40c1f

Please sign in to comment.