diff --git a/app/models/framework.rb b/app/models/framework.rb index 31be230598..6bd1a5e348 100644 --- a/app/models/framework.rb +++ b/app/models/framework.rb @@ -1,4 +1,6 @@ class Framework < ApplicationRecord + has_many :lots, dependent: :destroy + scope :supply_teachers, -> { where(service: 'supply_teachers').order(live_at: :asc) } scope :management_consultancy, -> { where(service: 'management_consultancy').order(live_at: :asc) } scope :legal_services, -> { where(service: 'legal_services').order(live_at: :asc) } diff --git a/app/models/framework/lot.rb b/app/models/framework/lot.rb new file mode 100644 index 0000000000..8d986e1e56 --- /dev/null +++ b/app/models/framework/lot.rb @@ -0,0 +1,4 @@ +class Framework::Lot < ApplicationRecord + belongs_to :framework + has_many :services, foreign_key: :framework_lot_id, dependent: :destroy, inverse_of: :lot +end diff --git a/app/models/framework/lot/service.rb b/app/models/framework/lot/service.rb new file mode 100644 index 0000000000..4c892995eb --- /dev/null +++ b/app/models/framework/lot/service.rb @@ -0,0 +1,3 @@ +class Framework::Lot::Service < ApplicationRecord + belongs_to :lot, foreign_key: :framework_lot_id, inverse_of: :services +end diff --git a/app/models/region.rb b/app/models/region.rb new file mode 100644 index 0000000000..2244fc9432 --- /dev/null +++ b/app/models/region.rb @@ -0,0 +1,2 @@ +class Region < ApplicationRecord +end diff --git a/data/frameworks/lots.csv b/data/frameworks/lots.csv new file mode 100644 index 0000000000..f4791a1e7b --- /dev/null +++ b/data/frameworks/lots.csv @@ -0,0 +1,17 @@ +framework_id,number,name +RM6187,1,Business +RM6187,2,Strategy and Policy +RM6187,3,Complex and Transformation +RM6187,4,Finance +RM6187,5,HR +RM6187,6,Procurement and Supply Chain +RM6187,7,"Health, Social Care and Community" +RM6187,8,Infrastructure including Transport +RM6187,9,Environmental Sustainability and Socio-economic Development +RM6240,1,Full service provision +RM6240,2,General service provision +RM6240,3,Transport rail legal services +RM6238,1,Direct provision +RM6238,2.1,Master vendor (less than 2.5 million) +RM6238,2.2,Master vendor (more than 2.5 million) +RM6238,4,Education technology platforms diff --git a/data/frameworks/lots/services.csv b/data/frameworks/lots/services.csv new file mode 100644 index 0000000000..18d378e7ff --- /dev/null +++ b/data/frameworks/lots/services.csv @@ -0,0 +1,188 @@ +framework_id,framework_lot_number,number,name +RM6187,1,1,Business case development +RM6187,1,2,Business consultancy +RM6187,1,3,Business continuity and/or disaster recovery planning +RM6187,1,4,Business policy strategy and/or appraisal +RM6187,1,5,Business processes +RM6187,1,6,Change management +RM6187,1,7,Development and/or review of policy +RM6187,1,8,"Digital, technology and cyber" +RM6187,1,9,Forecasting and/or planning +RM6187,1,10,Operational planning and/or improvement +RM6187,1,11,Organisational review +RM6187,1,12,Programme & project management +RM6187,1,13,"Risk, compliance and/or opportunity management" +RM6187,1,14,Value for money reviews. +RM6187,2,1,Business structure +RM6187,2,2,Business case development +RM6187,2,3,Business process re-engineering +RM6187,2,4,Change management +RM6187,2,5,"Digital, technology and cyber" +RM6187,2,6,Policy +RM6187,2,7,Regulatory advice +RM6187,2,8,Social value +RM6187,2,9,Strategic advice +RM6187,3,1,Business +RM6187,3,2,Change management +RM6187,3,3,Complex programmes +RM6187,3,4,"Digital, technology and cyber" +RM6187,3,5,Finance +RM6187,3,6,HR +RM6187,3,7,Organisation and operating model +RM6187,3,8,Performance transformation +RM6187,3,9,Procurement and/or supply chain +RM6187,3,10,Project and programme management +RM6187,3,11,Strategy and/or policy +RM6187,3,12,Supplier side services and delivery +RM6187,3,13,Transformation management. +RM6187,4,1,Actuarial services +RM6187,4,2,"Asset management including valuation, sales and disposals" +RM6187,4,3,Business analysis +RM6187,4,4,"Capital fundraising, derivatives and hedging" +RM6187,4,5,Cash management +RM6187,4,6,Corporate restructuring and flotation +RM6187,4,7,"Cost benefit reviews, studies, analysis and evaluation" +RM6187,4,8,"Debt restructuring, management and insolvency" +RM6187,4,9,Developing and assessing project proposals +RM6187,4,10,Financial due diligence +RM6187,4,11,Economic analysis +RM6187,4,12,Financial accounting and/or reporting +RM6187,4,13,Financial performance review and viability studies +RM6187,4,14,Financing public infrastructure projects and negotiations +RM6187,4,15,Forecasting and budgeting +RM6187,4,16,Foreign exchange +RM6187,4,17,"Investment, financial advice and market services" +RM6187,4,18,"Mergers, acquisitions and divestment" +RM6187,4,19,Payment structure advice and risk +RM6187,4,20,Pensions services +RM6187,4,21,Policy impact assessments +RM6187,4,22,Regulation and statutory requirements and/or reporting +RM6187,4,23,Risk management +RM6187,4,24,Tax including value added tax (VAT) +RM6187,5,1,Capability development +RM6187,5,2,Cultural transformation +RM6187,5,3,Dispute management +RM6187,5,4,Diversity and inclusion +RM6187,5,5,Employee relations +RM6187,5,6,"HR functions, process and design" +RM6187,5,7,HR policy +RM6187,5,8,Organisational design and/or workforce planning +RM6187,5,9,Performance management +RM6187,5,10,Training and development +RM6187,6,1,Category management +RM6187,6,2,Commercial review +RM6187,6,3,Contract management +RM6187,6,4,Cost reduction +RM6187,6,5,Digitalisation +RM6187,6,6,Financial advice +RM6187,6,7,Outsourcing and insourcing +RM6187,6,8,P2P +RM6187,6,9,Procurement process +RM6187,6,10,Sourcing +RM6187,6,11,Supplier management +RM6187,6,12,Supply chain and logistics +RM6187,6,13,Tender development and analysis +RM6187,7,1,Alternative delivery models +RM6187,7,2,Business case development +RM6187,7,3,Charity/third sector improvement review +RM6187,7,4,Capability development +RM6187,7,5,Clinical evaluations +RM6187,7,6,Commissioning models +RM6187,7,7,Community services +RM6187,7,8,"Digital, technology and cyber" +RM6187,7,9,Healthcare services +RM6187,7,10,"Healthcare operational review, improvement and/or modelling" +RM6187,7,11,"Healthcare transformation, change and delivery" +RM6187,7,12,Housing +RM6187,7,13,Mental healthcare +RM6187,7,14,"Planning for health, social care and community" +RM6187,7,15,Policing and security +RM6187,7,16,Programme and project management +RM6187,7,17,Public service improvement review +RM6187,7,18,Regeneration +RM6187,7,19,Safeguarding +RM6187,7,20,Social care services +RM6187,7,21,Strategy and policy +RM6187,8,1,Aviation +RM6187,8,2,Communications and technology infrastructure +RM6187,8,3,Highways +RM6187,8,4,Public transport (including buses and parking) +RM6187,8,5,Rail +RM6187,8,6,Ports and shipping +RM6187,8,7,Smart infrastructure +RM6187,8,8,Towns and cities +RM6187,9,1,Air quality +RM6187,9,2,Carbon management (including reporting) +RM6187,9,3,Climate change adaptation and/or mitigation +RM6187,9,4,Coastal +RM6187,9,5,Contaminated land +RM6187,9,6,Due diligence +RM6187,9,7,Ecology and biodiversity +RM6187,9,8,Environmental planning and protection +RM6187,9,9,Equality analysis +RM6187,9,10,Feasibility studies and/or impact assessment +RM6187,9,11,Monitoring environmental indicators +RM6187,9,12,Natural capital +RM6187,9,13,Natural resource management +RM6187,9,14,Policy development and/or implementation +RM6187,9,15,Pollution control (including noise) +RM6187,9,16,Regulatory compliance +RM6187,9,17,Risk management +RM6187,9,18,Social value +RM6187,9,19,Sustainability +RM6240,1,1,Administrative and Public Law +RM6240,1,2,Non-Complex Finance and Investment +RM6240,1,3,Contracts +RM6240,1,4,Competition Law +RM6240,1,5,Corporate Law +RM6240,1,6,Data Protection and Information Law +RM6240,1,7,Employment +RM6240,1,8,Information Technology +RM6240,1,9,Infrastructure +RM6240,1,10,Intellectual Property +RM6240,1,11,Litigation and Dispute Resolution +RM6240,1,12,Partnerships +RM6240,1,13,Pensions +RM6240,1,14,Public Procurement +RM6240,1,15,"Property, Real Estate and Construction" +RM6240,1,16,"Energy, Natural Resources and Climate Change" +RM6240,1,17,Retained EU Law and EU Law +RM6240,1,18,Planning +RM6240,1,19,Projects +RM6240,1,20,Restructuring and Insolvency +RM6240,1,21,Education Law +RM6240,1,22,Children and Vulnerable Adults +RM6240,1,23,"Food, Rural and Environmental Affairs" +RM6240,1,24,Franchise Law +RM6240,1,25,"Health, Healthcare and Social Care" +RM6240,1,26,Life Sciences +RM6240,1,27,Telecommunications +RM6240,1,28,"International Trade, Investment and Regulation" +RM6240,1,29,Public International Law +RM6240,1,30,Charities Law +RM6240,1,31,Health and Safety +RM6240,1,32,Licensing Law +RM6240,1,33,Transport Law (excluding Rail) +RM6240,1,34,Tax +RM6240,1,35,Outsourcing / Insourcing +RM6240,1,36,Islamic Finance / Sukuk +RM6240,1,37,Media Law +RM6240,1,38,Immigration +RM6240,1,39,Public Inquests and Inquiries +RM6240,1,40,Mental Health Law +RM6240,2,1,Property and Construction +RM6240,2,2,Social Housing +RM6240,2,3,Child Law +RM6240,2,4,Court of Protection +RM6240,2,5,Education Law +RM6240,2,6,Debt Recovery +RM6240,2,7,Planning and Environment +RM6240,2,8,Licensing +RM6240,2,9,Pensions +RM6240,2,10,Litigation / Dispute Resolution +RM6240,2,11,Intellectual Property +RM6240,2,12,Employment +RM6240,2,13,Healthcare +RM6240,2,14,Primary Care +RM6240,2,15,Mental Health Law +RM6240,3,1,Transport (Rail) \ No newline at end of file diff --git a/data/regions.csv b/data/regions.csv new file mode 100644 index 0000000000..48dc0e05fe --- /dev/null +++ b/data/regions.csv @@ -0,0 +1,5 @@ +id,name +a,England & Wales +b,Scotland +c,Northern Ireland +d,All regions \ No newline at end of file diff --git a/db/migrate/20240906101200_add_lot_and_service_tables.rb b/db/migrate/20240906101200_add_lot_and_service_tables.rb new file mode 100644 index 0000000000..74af698a5d --- /dev/null +++ b/db/migrate/20240906101200_add_lot_and_service_tables.rb @@ -0,0 +1,25 @@ +class AddLotAndServiceTables < ActiveRecord::Migration[7.1] + def change + create_table :framework_lots, id: :string, limit: 100 do |t| + t.references :framework, type: :string, foreign_key: true, null: false + t.string :number, limit: 6, null: false, index: true + t.text :name, null: false + + t.timestamps + end + + create_table :framework_lot_services, id: :string, limit: 100 do |t| + t.references :framework_lot, type: :string, foreign_key: true, null: false + t.string :number, limit: 6, null: false, index: true + t.text :name, null: false + + t.timestamps + end + + create_table :regions, id: :string, limit: 6 do |t| + t.text :name + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e8e1280f1..c7df4129bb 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.1].define(version: 2024_09_06_101133) do +ActiveRecord::Schema[7.1].define(version: 2024_09_06_101200) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -43,6 +43,26 @@ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "framework_lot_services", id: { type: :string, limit: 100 }, force: :cascade do |t| + t.string "framework_lot_id", null: false + t.string "number", limit: 6, null: false + t.text "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["framework_lot_id"], name: "index_framework_lot_services_on_framework_lot_id" + t.index ["number"], name: "index_framework_lot_services_on_number" + end + + create_table "framework_lots", id: { type: :string, limit: 100 }, force: :cascade do |t| + t.string "framework_id", null: false + t.string "number", limit: 6, null: false + t.text "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["framework_id"], name: "index_framework_lots_on_framework_id" + t.index ["number"], name: "index_framework_lots_on_number" + end + create_table "frameworks", id: { type: :string, limit: 6 }, force: :cascade do |t| t.string "service", limit: 25 t.date "live_at" @@ -152,6 +172,12 @@ t.datetime "updated_at", precision: nil, null: false end + create_table "regions", id: { type: :string, limit: 6 }, force: :cascade do |t| + t.text "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "supply_teachers_rm6238_admin_current_data", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.text "error" t.datetime "created_at", null: false @@ -238,6 +264,8 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "framework_lot_services", "framework_lots" + add_foreign_key "framework_lots", "frameworks" add_foreign_key "legal_services_rm6240_rates", "legal_services_rm6240_suppliers" add_foreign_key "legal_services_rm6240_service_offerings", "legal_services_rm6240_suppliers" add_foreign_key "management_consultancy_rm6187_rate_cards", "management_consultancy_rm6187_suppliers" diff --git a/lib/tasks/frameworks.rake b/lib/tasks/frameworks.rake index d7b61712f0..b0b3ba00fd 100644 --- a/lib/tasks/frameworks.rake +++ b/lib/tasks/frameworks.rake @@ -1,24 +1,52 @@ require_relative 'distributed_locks' module Frameworks - def self.add_frameworks - ActiveRecord::Base.connection.truncate_tables(:frameworks) + def self.remove_framework_tables + ActiveRecord::Base.connection.truncate_tables(:frameworks, :framework_lots, :framework_lot_services) + end + def self.add_frameworks CSV.foreach('data/frameworks.csv', headers: true) do |row| - Framework.create(id: row['id'], service: row['service'], live_at: Time.parse(row['live_at']).in_time_zone('London'), expires_at: Rails.env.test? ? 1.year.from_now : Time.parse(row['expires_at']).in_time_zone('London')) + Framework.create!( + **row, + live_at: Time.parse(row['live_at']).in_time_zone('London'), + expires_at: Rails.env.test? ? 1.year.from_now : Time.parse(row['expires_at']).in_time_zone('London') + ) + end + end + + def self.add_framework_lots + CSV.foreach('data/frameworks/lots.csv', headers: true) do |row| + Framework::Lot.create!( + id: "#{row['framework_id']}_#{row['number']}", + **row + ) + end + end + + def self.add_framework_lot_services + CSV.foreach('data/frameworks/lots/services.csv', headers: true) do |row| + Framework::Lot::Service.create!( + id: "#{row['framework_id']}_#{row['framework_lot_number']}_#{row['number']}", + framework_lot_id: "#{row['framework_id']}_#{row['framework_lot_number']}", + **row.to_h.except('framework_id', 'framework_lot_number') + ) end end end namespace :db do desc 'add the frameworks into the database' - task legacy_frameworks: :environment do - puts 'Loading Legacy Frameworks' + task frameworks: :environment do + puts 'Loading Frameworks' DistributedLocks.distributed_lock(157) do + Frameworks.remove_framework_tables Frameworks.add_frameworks + Frameworks.add_framework_lots + Frameworks.add_framework_lot_services end end desc 'add static data to the database' - task static: :legacy_frameworks + task static: :frameworks end diff --git a/lib/tasks/regions.rake b/lib/tasks/regions.rake new file mode 100644 index 0000000000..d0d9359932 --- /dev/null +++ b/lib/tasks/regions.rake @@ -0,0 +1,24 @@ +require_relative 'distributed_locks' + +module Regions + def self.add_regions + ActiveRecord::Base.connection.truncate_tables(:regions) + + CSV.foreach('data/regions.csv', headers: true) do |row| + Region.create(**row) + end + end +end + +namespace :db do + desc 'add the regions into the database' + task regions: :environment do + puts 'Loading Regions' + DistributedLocks.distributed_lock(158) do + Regions.add_regions + end + end + + desc 'add static data to the database' + task static: :regions +end diff --git a/lib/tasks/static.rake b/lib/tasks/static.rake new file mode 100644 index 0000000000..b5e34b1a92 --- /dev/null +++ b/lib/tasks/static.rake @@ -0,0 +1,7 @@ +namespace :db do + desc 'add static data to the database' + task static: :environment + + desc 'add static data to the database during setup' + task setup: :static +end diff --git a/spec/models/framework/lot/service_spec.rb b/spec/models/framework/lot/service_spec.rb new file mode 100644 index 0000000000..8990896156 --- /dev/null +++ b/spec/models/framework/lot/service_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Framework::Lot::Service do + it { is_expected.to belong_to(:lot) } + + describe 'when considering the framework' do + let(:result) { Framework::Lot.find("#{framework}_#{number}").services.count } + + context 'and the framework is RM6187' do + let(:framework) { 'RM6187' } + + [ + ['1', 14], + ['2', 9], + ['3', 13], + ['4', 24], + ['5', 10], + ['6', 13], + ['7', 21], + ['8', 8], + ['9', 19], + ].each do |lot_number, expected_result| + context "and the lot is #{lot_number}" do + let(:number) { lot_number } + + it 'has the correct number of services' do + expect(result).to eq(expected_result) + end + end + end + end + + context 'and the framework is RM6240' do + let(:framework) { 'RM6240' } + + [ + ['1', 40], + ['2', 15], + ['3', 1], + ].each do |lot_number, expected_result| + context "and the lot is #{lot_number}" do + let(:number) { lot_number } + + it 'has the correct number of services' do + expect(result).to eq(expected_result) + end + end + end + end + + context 'and the framework is RM6238' do + let(:framework) { 'RM6238' } + + [ + ['1', 0], + ['2.1', 0], + ['2.2', 0], + ['4', 0], + ].each do |lot_number, expected_result| + context "and the lot is #{lot_number}" do + let(:number) { lot_number } + + it 'has the correct number of services' do + expect(result).to eq(expected_result) + end + end + end + end + end +end diff --git a/spec/models/framework/lot_spec.rb b/spec/models/framework/lot_spec.rb new file mode 100644 index 0000000000..5ac540a7b4 --- /dev/null +++ b/spec/models/framework/lot_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe Framework::Lot do + it { is_expected.to belong_to(:framework) } + it { is_expected.to have_many(:services) } + + describe 'when considering the framework' do + let(:result) { Framework.find(framework).lots.count } + + context 'and the framework is RM6187' do + let(:framework) { 'RM6187' } + + it 'has the correct number of lots' do + expect(result).to eq(9) + end + end + + context 'and the framework is RM6240' do + let(:framework) { 'RM6240' } + + it 'has the correct number of lots' do + expect(result).to eq(3) + end + end + + context 'and the framework is RM6238' do + let(:framework) { 'RM6238' } + + it 'has the correct number of lots' do + expect(result).to eq(4) + end + end + end +end diff --git a/spec/models/framework_spec.rb b/spec/models/framework_spec.rb index 0a9e5a1672..85c42a8f3d 100644 --- a/spec/models/framework_spec.rb +++ b/spec/models/framework_spec.rb @@ -2,6 +2,8 @@ # rubocop:disable RSpec/NestedGroups RSpec.describe Framework do + it { is_expected.to have_many(:lots) } + describe '.frameworks' do context 'when no scope is provided' do it 'returns RM6238, RM6187 and RM6240' do