diff --git a/app/models/batch.rb b/app/models/batch.rb index 0ea89b2fd..ec04e135f 100644 --- a/app/models/batch.rb +++ b/app/models/batch.rb @@ -28,8 +28,6 @@ class Batch < ApplicationRecord has_and_belongs_to_many :immunisation_imports - has_many :programmes, through: :vaccine - validates :name, presence: true validates :expiry, presence: true end diff --git a/app/models/programme.rb b/app/models/programme.rb index 3b9391017..3710e0900 100644 --- a/app/models/programme.rb +++ b/app/models/programme.rb @@ -19,7 +19,6 @@ class Programme < ApplicationRecord audited has_and_belongs_to_many :sessions - has_and_belongs_to_many :vaccines has_many :consent_forms has_many :consents @@ -28,6 +27,7 @@ class Programme < ApplicationRecord has_many :team_programmes has_many :triages has_many :vaccination_records + has_many :vaccines has_many :batches, through: :vaccines has_many :patient_sessions, through: :sessions @@ -36,34 +36,13 @@ class Programme < ApplicationRecord enum :type, { flu: "flu", hpv: "hpv" }, validate: true - validate :vaccines_match_type - def name human_enum_name(:type) end - def vaccine_ids - @vaccine_ids ||= vaccines.map(&:id) - end - - def vaccine_ids=(ids) - self.vaccines = Vaccine.where(id: ids) - end - YEAR_GROUPS_BY_TYPE = { "flu" => (0..11).to_a, "hpv" => (8..11).to_a }.freeze def year_groups YEAR_GROUPS_BY_TYPE.fetch(type) end - - private - - def vaccines_match_type - errors.add(:vaccines, :blank) if vaccines.empty? - - vaccine_types = vaccines.map(&:type).uniq - unless vaccine_types.empty? || vaccine_types == [type] - errors.add(:vaccines, :match_type) - end - end end diff --git a/app/models/vaccine.rb b/app/models/vaccine.rb index 579cba982..a8e8a9809 100644 --- a/app/models/vaccine.rb +++ b/app/models/vaccine.rb @@ -17,20 +17,28 @@ # type :string not null # created_at :datetime not null # updated_at :datetime not null +# programme_id :bigint not null # # Indexes # # index_vaccines_on_gtin (gtin) UNIQUE # index_vaccines_on_manufacturer_and_brand (manufacturer,brand) UNIQUE # index_vaccines_on_nivs_name (nivs_name) UNIQUE +# index_vaccines_on_programme_id (programme_id) # index_vaccines_on_snomed_product_code (snomed_product_code) UNIQUE # index_vaccines_on_snomed_product_term (snomed_product_term) UNIQUE # +# Foreign Keys +# +# fk_rails_... (programme_id => programmes.id) +# class Vaccine < ApplicationRecord self.inheritance_column = nil audited + belongs_to :programme + has_and_belongs_to_many :programmes has_many :health_questions, dependent: :destroy has_many :batches, -> { order(:name) } diff --git a/app/policies/batch_policy.rb b/app/policies/batch_policy.rb index 4af73d1fe..51ce4c42f 100644 --- a/app/policies/batch_policy.rb +++ b/app/policies/batch_policy.rb @@ -8,7 +8,11 @@ def initialize(user, scope) end def resolve - @scope.joins(:programmes).where(programmes: @user.programmes) + @scope.joins(vaccine: :programme).where( + vaccine: { + programme: @user.programmes + } + ) end end end diff --git a/app/policies/vaccine_policy.rb b/app/policies/vaccine_policy.rb index 6b5d7716d..31eae1ee7 100644 --- a/app/policies/vaccine_policy.rb +++ b/app/policies/vaccine_policy.rb @@ -8,7 +8,7 @@ def initialize(user, scope) end def resolve - @scope.joins(:programmes).where(programmes: @user.programmes) + @scope.joins(:programme).where(programme: @user.programmes) end end end diff --git a/db/migrate/20240927134753_add_programme_to_vaccines.rb b/db/migrate/20240927134753_add_programme_to_vaccines.rb new file mode 100644 index 000000000..c1eec52b7 --- /dev/null +++ b/db/migrate/20240927134753_add_programme_to_vaccines.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddProgrammeToVaccines < ActiveRecord::Migration[7.2] + def up + add_reference :vaccines, :programme, foreign_key: true + + Vaccine.all.find_each do |vaccine| + vaccine.update!( + programme: Programme.find_or_create_by!(type: vaccine.type) + ) + end + + change_column_null :vaccines, :programme_id, false + end + + def down + remove_reference :vaccines, :programme + end +end diff --git a/db/schema.rb b/db/schema.rb index f4026e67b..b14d47a88 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.2].define(version: 2024_09_27_124718) do +ActiveRecord::Schema[7.2].define(version: 2024_09_27_134753) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -599,9 +599,11 @@ t.string "snomed_product_term", null: false t.text "nivs_name", null: false t.boolean "discontinued", default: false, null: false + t.bigint "programme_id", null: false t.index ["gtin"], name: "index_vaccines_on_gtin", unique: true t.index ["manufacturer", "brand"], name: "index_vaccines_on_manufacturer_and_brand", unique: true t.index ["nivs_name"], name: "index_vaccines_on_nivs_name", unique: true + t.index ["programme_id"], name: "index_vaccines_on_programme_id" t.index ["snomed_product_code"], name: "index_vaccines_on_snomed_product_code", unique: true t.index ["snomed_product_term"], name: "index_vaccines_on_snomed_product_term", unique: true end @@ -668,4 +670,5 @@ add_foreign_key "vaccination_records", "programmes" add_foreign_key "vaccination_records", "users", column: "performed_by_user_id" add_foreign_key "vaccination_records", "vaccines" + add_foreign_key "vaccines", "programmes" end diff --git a/erd.pdf b/erd.pdf index 774e03f23..466e23d8e 100644 Binary files a/erd.pdf and b/erd.pdf differ diff --git a/spec/components/app_vaccination_record_summary_component_spec.rb b/spec/components/app_vaccination_record_summary_component_spec.rb index 8067d6335..d1dcd6d15 100644 --- a/spec/components/app_vaccination_record_summary_component_spec.rb +++ b/spec/components/app_vaccination_record_summary_component_spec.rb @@ -7,10 +7,10 @@ let(:administered_at) { Time.zone.local(2024, 9, 6, 12) } let(:location) { create(:location, :school, name: "Hogwarts") } - let(:programme) { create(:programme, type: vaccine&.type || :hpv) } + let(:programme) { create(:programme, :hpv) } let(:session) { create(:session, programme:, location:) } let(:patient_session) { create(:patient_session, session:) } - let(:vaccine) { create(:vaccine, :gardasil_9) } + let(:vaccine) { programme.vaccines.first } let(:batch) do create(:batch, name: "ABC", expiry: Date.new(2020, 1, 1), vaccine:) end diff --git a/spec/factories/programmes.rb b/spec/factories/programmes.rb index 23cf5fdf7..4edc4b56c 100644 --- a/spec/factories/programmes.rb +++ b/spec/factories/programmes.rb @@ -18,20 +18,24 @@ transient { batch_count { 1 } } type { %w[flu hpv].sample } - vaccines { [association(:vaccine, type:, batch_count:)] } + vaccines do + [association(:vaccine, type:, batch_count:, programme: instance)] + end trait :hpv do type { "hpv" } - vaccines { [association(:vaccine, :gardasil_9, batch_count:)] } + vaccines do + [association(:vaccine, :gardasil_9, batch_count:, programme: instance)] + end end trait :hpv_all_vaccines do hpv vaccines do [ - association(:vaccine, :cervarix, batch_count:), - association(:vaccine, :gardasil, batch_count:), - association(:vaccine, :gardasil_9, batch_count:) + association(:vaccine, :cervarix, batch_count:, programme: instance), + association(:vaccine, :gardasil, batch_count:, programme: instance), + association(:vaccine, :gardasil_9, batch_count:, programme: instance) ] end end @@ -45,12 +49,37 @@ type { "flu" } vaccines do [ - association(:vaccine, :adjuvanted_quadrivalent, batch_count:), - association(:vaccine, :cell_quadrivalent, batch_count:), - association(:vaccine, :fluenz_tetra, batch_count:), - association(:vaccine, :quadrivalent_influenza, batch_count:), - association(:vaccine, :quadrivalent_influvac_tetra, batch_count:), - association(:vaccine, :supemtek, batch_count:) + association( + :vaccine, + :adjuvanted_quadrivalent, + batch_count:, + programme: instance + ), + association( + :vaccine, + :cell_quadrivalent, + batch_count:, + programme: instance + ), + association( + :vaccine, + :fluenz_tetra, + batch_count:, + programme: instance + ), + association( + :vaccine, + :quadrivalent_influenza, + batch_count:, + programme: instance + ), + association( + :vaccine, + :quadrivalent_influvac_tetra, + batch_count:, + programme: instance + ), + association(:vaccine, :supemtek, batch_count:, programme: instance) ] end end @@ -59,21 +88,65 @@ flu vaccines do [ - association(:vaccine, :adjuvanted_quadrivalent, batch_count:), - association(:vaccine, :cell_quadrivalent, batch_count:), - association(:vaccine, :fluad_tetra, batch_count:), - association(:vaccine, :flucelvax_tetra, batch_count:), - association(:vaccine, :fluenz_tetra, batch_count:), - association(:vaccine, :quadrivalent_influenza, batch_count:), - association(:vaccine, :quadrivalent_influvac_tetra, batch_count:), - association(:vaccine, :supemtek, batch_count:) + association( + :vaccine, + :adjuvanted_quadrivalent, + batch_count:, + programme: instance + ), + association( + :vaccine, + :cell_quadrivalent, + batch_count:, + programme: instance + ), + association( + :vaccine, + :fluad_tetra, + batch_count:, + programme: instance + ), + association( + :vaccine, + :flucelvax_tetra, + batch_count:, + programme: instance + ), + association( + :vaccine, + :fluenz_tetra, + batch_count:, + programme: instance + ), + association( + :vaccine, + :quadrivalent_influenza, + batch_count:, + programme: instance + ), + association( + :vaccine, + :quadrivalent_influvac_tetra, + batch_count:, + programme: instance + ), + association(:vaccine, :supemtek, batch_count:, programme: instance) ] end end trait :flu_nasal_only do flu - vaccines { [association(:vaccine, :fluenz_tetra, batch_count:)] } + vaccines do + [ + association( + :vaccine, + :fluenz_tetra, + batch_count:, + programme: instance + ) + ] + end end end end diff --git a/spec/factories/vaccines.rb b/spec/factories/vaccines.rb index 0a2952432..c93da192b 100644 --- a/spec/factories/vaccines.rb +++ b/spec/factories/vaccines.rb @@ -17,20 +17,28 @@ # type :string not null # created_at :datetime not null # updated_at :datetime not null +# programme_id :bigint not null # # Indexes # # index_vaccines_on_gtin (gtin) UNIQUE # index_vaccines_on_manufacturer_and_brand (manufacturer,brand) UNIQUE # index_vaccines_on_nivs_name (nivs_name) UNIQUE +# index_vaccines_on_programme_id (programme_id) # index_vaccines_on_snomed_product_code (snomed_product_code) UNIQUE # index_vaccines_on_snomed_product_term (snomed_product_term) UNIQUE # +# Foreign Keys +# +# fk_rails_... (programme_id => programmes.id) +# FactoryBot.define do factory :vaccine do transient { batch_count { 1 } } type { %w[flu hpv].sample } + programme { Programme.find_or_create_by!(type:) } + brand { Faker::Commerce.product_name } manufacturer { Faker::Company.name } sequence(:nivs_name) { |n| "#{brand.parameterize}-#{n}" } diff --git a/spec/features/manage_vaccines_spec.rb b/spec/features/manage_vaccines_spec.rb index 2a16cee4b..e58052262 100644 --- a/spec/features/manage_vaccines_spec.rb +++ b/spec/features/manage_vaccines_spec.rb @@ -14,9 +14,8 @@ end def given_my_team_is_running_an_hpv_vaccination_programme - @programme = create(:programme, :hpv_no_batches) - @team = create(:team, :with_one_nurse, programmes: [@programme]) - @vaccine = @programme.vaccines.first + programme = create(:programme, :hpv_no_batches) + @team = create(:team, :with_one_nurse, programmes: [programme]) end def when_i_manage_vaccines diff --git a/spec/features/user_authorisation_spec.rb b/spec/features/user_authorisation_spec.rb index cf955d024..87c160bb4 100644 --- a/spec/features/user_authorisation_spec.rb +++ b/spec/features/user_authorisation_spec.rb @@ -18,8 +18,7 @@ end def given_an_hpv_programme_is_underway_with_two_teams - vaccine = create(:vaccine, :hpv) - programme = create(:programme, :hpv, vaccines: [vaccine]) + programme = create(:programme, :hpv) @team = create(:team, :with_one_nurse, programmes: [programme]) @other_team = create(:team, :with_one_nurse, programmes: [programme]) diff --git a/spec/models/dps_export_row_spec.rb b/spec/models/dps_export_row_spec.rb index b37841907..9f3924a52 100644 --- a/spec/models/dps_export_row_spec.rb +++ b/spec/models/dps_export_row_spec.rb @@ -3,11 +3,9 @@ describe DPSExportRow do subject(:row) { described_class.new(vaccination_record) } - let(:programme) do - create(:programme, type: vaccine.type, vaccines: [vaccine]) - end + let(:programme) { create(:programme, type: "hpv") } let(:team) { create(:team, programmes: [programme]) } - let(:vaccine) { create(:vaccine, :gardasil_9, dose: 0.5) } + let(:vaccine) { create(:vaccine, :gardasil_9, programme:, dose: 0.5) } let(:location) { create(:location, :school) } let(:school) { create(:location, :school) } let(:patient) do @@ -188,11 +186,12 @@ end context "when the vaccine is a nasal spray" do - let(:vaccine) { create :vaccine, :fluenz_tetra } + let(:vaccine) { create(:vaccine, :fluenz_tetra, programme:) } let(:vaccination_record) do create( :vaccination_record, + programme:, vaccine:, batch: create(:batch, vaccine:), delivery_site: :nose, @@ -210,7 +209,7 @@ end context "when the vaccine is an intramuscular injection" do - let(:vaccine) { create :vaccine, :quadrivalent_influenza } + let(:vaccine) { create(:vaccine, :quadrivalent_influenza, programme:) } it "has route_of_vaccination_code" do expect(array[26]).to eq "78421000" diff --git a/spec/models/programme_spec.rb b/spec/models/programme_spec.rb index 0cc8979cb..4edb28481 100644 --- a/spec/models/programme_spec.rb +++ b/spec/models/programme_spec.rb @@ -20,19 +20,6 @@ describe "validations" do it { should validate_presence_of(:type) } it { should validate_inclusion_of(:type).in_array(%w[flu hpv]) } - - context "when vaccines don't match type" do - subject(:programme) do - build(:programme, type: "flu", vaccines: [build(:vaccine, type: "hpv")]) - end - - it "is invalid" do - expect(programme).to be_invalid - expect(programme.errors[:vaccines]).to include( - /must be suitable for the programme type/ - ) - end - end end describe "#name" do diff --git a/spec/models/vaccine_spec.rb b/spec/models/vaccine_spec.rb index ff5e680b2..142505923 100644 --- a/spec/models/vaccine_spec.rb +++ b/spec/models/vaccine_spec.rb @@ -17,15 +17,21 @@ # type :string not null # created_at :datetime not null # updated_at :datetime not null +# programme_id :bigint not null # # Indexes # # index_vaccines_on_gtin (gtin) UNIQUE # index_vaccines_on_manufacturer_and_brand (manufacturer,brand) UNIQUE # index_vaccines_on_nivs_name (nivs_name) UNIQUE +# index_vaccines_on_programme_id (programme_id) # index_vaccines_on_snomed_product_code (snomed_product_code) UNIQUE # index_vaccines_on_snomed_product_term (snomed_product_term) UNIQUE # +# Foreign Keys +# +# fk_rails_... (programme_id => programmes.id) +# describe Vaccine, type: :model do describe "validation" do diff --git a/spec/policies/session_policy_spec.rb b/spec/policies/session_policy_spec.rb index 4b2a9c7c4..0b5ea8a18 100644 --- a/spec/policies/session_policy_spec.rb +++ b/spec/policies/session_policy_spec.rb @@ -9,7 +9,7 @@ let(:user) { create(:user, teams: [team]) } let(:users_teams_session) { create(:session, team:, programme:) } - let(:another_teams_session) { create(:session) } + let(:another_teams_session) { create(:session, programme:) } it { should include(users_teams_session) } it { should_not include(another_teams_session) }