diff --git a/app/jobs/email_delivery_job.rb b/app/jobs/email_delivery_job.rb index a683baf72c..165e9d6414 100644 --- a/app/jobs/email_delivery_job.rb +++ b/app/jobs/email_delivery_job.rb @@ -101,6 +101,7 @@ def perform( sent_by:, template_id: log_template_id, type: :email, + purpose: NotifyLogEntry.purpose_for_template_name(template_name_sym), notify_log_entry_programmes_attributes: personalisation.programmes.map do { programme_type: it.type, disease_types: it.disease_types } diff --git a/app/jobs/sms_delivery_job.rb b/app/jobs/sms_delivery_job.rb index a9b2cff17b..ef7b4ab57f 100644 --- a/app/jobs/sms_delivery_job.rb +++ b/app/jobs/sms_delivery_job.rb @@ -98,6 +98,7 @@ def perform( sent_by:, template_id: log_template_id, type: :sms, + purpose: NotifyLogEntry.purpose_for_template_name(template_name_sym), notify_log_entry_programmes_attributes: personalisation.programmes.map do { programme_type: it.type, disease_types: it.disease_types } diff --git a/app/lib/data_migration/backfill_notify_log_entries.rb b/app/lib/data_migration/backfill_notify_log_entries.rb new file mode 100644 index 0000000000..ba62e874d6 --- /dev/null +++ b/app/lib/data_migration/backfill_notify_log_entries.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +TEMPLATE_NAME_BY_TEMPLATE_ID = { + **GOVUK_NOTIFY_UNUSED_TEMPLATES, + **GOVUK_NOTIFY_EMAIL_TEMPLATES.invert, + **GOVUK_NOTIFY_SMS_TEMPLATES.invert +}.freeze + +class DataMigration::BackfillNotifyLogEntries + def call + progress_bar = + # rubocop:disable Rails/SaveBang + ProgressBar.create( + total: NotifyLogEntry.count, + format: "%a %b\u{15E7}%i %p%% %t", + progress_mark: " ", + remainder_mark: "\u{FF65}" + ) + # rubocop:enable Rails/SaveBang + + NotifyLogEntry.find_in_batches do |notify_log_entries| + notify_log_entries.filter_map do |notify_log_entry| + template_name = + TEMPLATE_NAME_BY_TEMPLATE_ID.fetch(notify_log_entry.template_id, nil) + + next unless template_name + + purpose = NotifyLogEntry.purpose_for_template_name(template_name) + + next unless purpose + + notify_log_entry.update_column( + :purpose, + NotifyLogEntry.purposes.fetch(purpose) + ) + end + + progress_bar.progress += 1 + end + + progress_bar.finish + end + + def self.call(...) = new(...).call + + private_class_method :new +end diff --git a/app/models/notify_log_entry.rb b/app/models/notify_log_entry.rb index 2031aa413f..90457a2783 100644 --- a/app/models/notify_log_entry.rb +++ b/app/models/notify_log_entry.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # delivery_status :integer default("sending"), not null +# purpose :integer # recipient :string not null # type :integer not null # created_at :datetime not null @@ -58,6 +59,24 @@ class NotifyLogEntry < ApplicationRecord not_uk_mobile_number_failure: 5 } + enum :purpose, + { + consent_request: 0, + consent_reminder: 1, + consent_confirmation: 2, + consent_warning: 3, + clinic_invitation: 4, + session_reminder: 5, + triage_vaccination_will_happen: 6, + triage_vaccination_wont_happen: 7, + triage_vaccination_at_clinic: 8, + triage_delay_vaccination: 9, + vaccination_administered: 10, + vaccination_already_had: 11, + vaccination_not_administered: 12, + vaccination_deleted: 13 + } + validates :recipient, presence: true validates :template_id, presence: true @@ -87,6 +106,40 @@ def title def programmes = notify_log_entry_programmes.map(&:programme) + def self.purpose_for_template_name(template_name_sym) + name = template_name_sym.to_s + + if name.include?("consent") && name.include?("request") + :consent_request + elsif name.include?("consent") && name.include?("reminder") + :consent_reminder + elsif name.include?("consent_confirmation") + :consent_confirmation + elsif name.include?("consent") && name.include?("warning") + :consent_warning + elsif name.include?("clinic") && name.include?("invitation") + :clinic_invitation + elsif name.include?("session_school_reminder") + :session_reminder + elsif name.include?("triage_vaccination_will_happen") + :triage_vaccination_will_happen + elsif name.include?("triage_vaccination_wont_happen") + :triage_vaccination_wont_happen + elsif name.include?("triage_vaccination_at_clinic") + :triage_vaccination_at_clinic + elsif name.include?("triage_delay_vaccination") + :triage_delay_vaccination + elsif name.include?("vaccination_administered") + :vaccination_administered + elsif name.include?("vaccination_already_had") + :vaccination_already_had + elsif name.include?("vaccination_not_administered") + :vaccination_not_administered + elsif name.include?("vaccination_deleted") + :vaccination_deleted + end + end + private def template_name diff --git a/db/migrate/20260303152926_add_purpose_to_notify_log_entries.rb b/db/migrate/20260303152926_add_purpose_to_notify_log_entries.rb new file mode 100644 index 0000000000..51e971c4a1 --- /dev/null +++ b/db/migrate/20260303152926_add_purpose_to_notify_log_entries.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPurposeToNotifyLogEntries < ActiveRecord::Migration[8.1] + def change + add_column :notify_log_entries, :purpose, :integer, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b70bc0cc95..4564660699 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[8.1].define(version: 2026_02_25_094039) do +ActiveRecord::Schema[8.1].define(version: 2026_03_03_152926) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_trgm" @@ -529,6 +529,7 @@ t.integer "delivery_status", default: 0, null: false t.bigint "parent_id" t.bigint "patient_id" + t.integer "purpose" t.string "recipient", null: false t.bigint "sent_by_user_id" t.uuid "template_id", null: false diff --git a/lib/tasks/data_migration.rake b/lib/tasks/data_migration.rake new file mode 100644 index 0000000000..4ccf184544 --- /dev/null +++ b/lib/tasks/data_migration.rake @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +namespace :data_migration do + desc "Backfill purpose column for NotifyLogEntry records" + task backfill_notify_log_entries: :environment do + DataMigration::BackfillNotifyLogEntries.call + end +end diff --git a/spec/factories/notify_log_entries.rb b/spec/factories/notify_log_entries.rb index 449fa866a1..4f5caeff76 100644 --- a/spec/factories/notify_log_entries.rb +++ b/spec/factories/notify_log_entries.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # delivery_status :integer default("sending"), not null +# purpose :integer # recipient :string not null # type :integer not null # created_at :datetime not null @@ -51,6 +52,7 @@ delivery_id { SecureRandom.uuid } traits_for_enum :delivery_status + traits_for_enum :purpose after(:build) do |notify_log_entry, evaluator| if notify_log_entry.notify_log_entry_programmes.empty? diff --git a/spec/jobs/sms_delivery_job_spec.rb b/spec/jobs/sms_delivery_job_spec.rb index 531a0981f8..e7d0c4c4c5 100644 --- a/spec/jobs/sms_delivery_job_spec.rb +++ b/spec/jobs/sms_delivery_job_spec.rb @@ -43,7 +43,7 @@ ) end - let(:template_name) { GOVUK_NOTIFY_SMS_TEMPLATES.keys.first } + let(:template_name) { :consent_school_request } let(:academic_year) { session.academic_year } let(:consent) { nil } let(:consent_form) { nil } @@ -92,6 +92,7 @@ expect(notify_log_entry.template_id).to eq( GOVUK_NOTIFY_SMS_TEMPLATES[template_name] ) + expect(notify_log_entry.purpose).to eq("consent_request") expect(notify_log_entry.parent).to eq(parent) expect(notify_log_entry.patient).to eq(patient) expect(notify_log_entry.programmes.map(&:type)).to eq(programme_types) @@ -130,6 +131,7 @@ expect(notify_log_entry.template_id).to eq( GOVUK_NOTIFY_SMS_TEMPLATES[template_name] ) + expect(notify_log_entry.purpose).to eq("consent_request") expect(notify_log_entry.parent).to eq(parent) expect(notify_log_entry.patient).to eq(patient) expect(notify_log_entry.programmes.map(&:type)).to eq(programme_types) @@ -160,6 +162,7 @@ expect(notify_log_entry.template_id).to eq( GOVUK_NOTIFY_SMS_TEMPLATES[template_name] ) + expect(notify_log_entry.purpose).to eq("consent_request") expect(notify_log_entry.parent).to eq(parent) expect(notify_log_entry.patient).to eq(patient) expect(notify_log_entry.programmes.map(&:type)).to eq(programme_types) @@ -193,6 +196,7 @@ expect(notify_log_entry.template_id).to eq( GOVUK_NOTIFY_SMS_TEMPLATES[template_name] ) + expect(notify_log_entry.purpose).to eq("consent_request") expect(notify_log_entry.consent_form).to eq(consent_form) expect(notify_log_entry.programmes.map(&:type)).to eq(programme_types) end @@ -246,6 +250,7 @@ expect(notify_log_entry.template_id).to eq( GOVUK_NOTIFY_SMS_TEMPLATES[template_name] ) + expect(notify_log_entry.purpose).to eq("consent_request") expect(notify_log_entry.consent_form).to eq(consent_form) expect(notify_log_entry.programmes.map(&:type)).to eq(programme_types) expect(notify_log_entry.sent_by).to eq(sent_by) diff --git a/spec/lib/data_migration/backfill_notify_log_entries_spec.rb b/spec/lib/data_migration/backfill_notify_log_entries_spec.rb new file mode 100644 index 0000000000..1983230296 --- /dev/null +++ b/spec/lib/data_migration/backfill_notify_log_entries_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe DataMigration::BackfillNotifyLogEntries do + describe ".call" do + let!(:mapped_entry) do + create( + :notify_log_entry, + :email, + template_id: "14e88a09-4281-4257-9574-6afeaeb42715", + purpose: nil + ) + end + let!(:unknown_entry) do + create( + :notify_log_entry, + :email, + template_id: "99999999-9999-9999-9999-999999999999", + purpose: nil + ) + end + + it "backfills purpose from historical template ID mappings" do + described_class.call + + expect(mapped_entry.reload.purpose).to eq("consent_request") + expect(unknown_entry.reload.purpose).to be_nil + end + end +end diff --git a/spec/models/notify_log_entry_spec.rb b/spec/models/notify_log_entry_spec.rb index 2f86702ad8..e1361d8411 100644 --- a/spec/models/notify_log_entry_spec.rb +++ b/spec/models/notify_log_entry_spec.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # delivery_status :integer default("sending"), not null +# purpose :integer # recipient :string not null # type :integer not null # created_at :datetime not null @@ -46,6 +47,102 @@ it { should be_valid } end + describe ".purpose_for_template_name" do + subject(:purpose) do + described_class.purpose_for_template_name(template_name) + end + + context "when the template indicates a consent request" do + let(:template_name) { :consent_school_request } + + it { should eq(:consent_request) } + end + + context "when the template indicates a consent reminder" do + let(:template_name) { :consent_school_reminder } + + it { should eq(:consent_reminder) } + end + + context "when the template indicates a consent confirmation" do + let(:template_name) { :consent_confirmation_given } + + it { should eq(:consent_confirmation) } + end + + context "when the template indicates a consent warning" do + let(:template_name) { :consent_unknown_contact_details_warning } + + it { should eq(:consent_warning) } + end + + context "when the template indicates a clinic invitation" do + let(:template_name) { :clinic_flu_invitation } + + it { should eq(:clinic_invitation) } + end + + context "when the template indicates a session reminder" do + let(:template_name) { :session_school_reminder_today } + + it { should eq(:session_reminder) } + end + + context "when the template indicates triage vaccination will happen" do + let(:template_name) { :triage_vaccination_will_happen_outcome } + + it { should eq(:triage_vaccination_will_happen) } + end + + context "when the template indicates triage vaccination won't happen" do + let(:template_name) { :triage_vaccination_wont_happen_outcome } + + it { should eq(:triage_vaccination_wont_happen) } + end + + context "when the template indicates triage vaccination at clinic" do + let(:template_name) { :triage_vaccination_at_clinic_outcome } + + it { should eq(:triage_vaccination_at_clinic) } + end + + context "when the template indicates a triage delay vaccination update" do + let(:template_name) { :triage_delay_vaccination_outcome } + + it { should eq(:triage_delay_vaccination) } + end + + context "when the template indicates vaccination administered" do + let(:template_name) { :vaccination_administered_notification } + + it { should eq(:vaccination_administered) } + end + + context "when the template indicates vaccination already had" do + let(:template_name) { :vaccination_already_had_notification } + + it { should eq(:vaccination_already_had) } + end + + context "when the template indicates vaccination not administered" do + let(:template_name) { :vaccination_not_administered_notification } + + it { should eq(:vaccination_not_administered) } + end + + context "when the template indicates vaccination deleted" do + let(:template_name) { :vaccination_deleted_notification } + + it { should eq(:vaccination_deleted) } + end + + context "when the template name does not match any known purpose" do + let(:template_name) { :something_else_entirely } + + it { should be_nil } + end + end + describe "#title" do subject(:title) { notify_log_entry.title }