From 38e9ca497917dd526472a5450647115da5922f2e Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 19 Nov 2024 12:51:40 +0100 Subject: [PATCH 1/3] [#10951] Create a cron job to warn user with a old brouillon --- ...old_brouillon_dossiers_soon_deleted_job.rb | 16 +++++ app/mailers/dossier_mailer.rb | 20 ++++++ ...otify_old_brouillon_soon_deleted.html.haml | 9 +++ .../notify_old_brouillon_soon_deleted/en.yml | 11 +++ .../notify_old_brouillon_soon_deleted/fr.yml | 11 +++ ...tified_soon_deleted_sent_at_to_dossiers.rb | 7 ++ db/schema.rb | 3 +- .../notify_old_brouillon_dossiers_job_spec.rb | 24 +++++++ ...rouillon_dossiers_soon_deleted_job_spec.rb | 53 +++++++++++++++ spec/mailers/dossier_mailer_spec.rb | 68 +++++++++++++++++++ 10 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb create mode 100644 app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml create mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml create mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml create mode 100644 db/migrate/20241126145420_add_notified_soon_deleted_sent_at_to_dossiers.rb create mode 100644 spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb create mode 100644 spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb diff --git a/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb b/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb new file mode 100644 index 00000000000..680d56b3d1c --- /dev/null +++ b/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Cron::NotifyOldBrouillonDossiersSoonDeletedJob < Cron::CronJob + self.schedule_expression = "every day at 9:00" + + def perform + Dossier + .state_brouillon + .where(updated_at: ..3.months.ago) + .where("notified_soon_deleted_sent_at IS NULL OR notified_soon_deleted_sent_at < updated_at") + .find_each do |dossier| + DossierMailer.notify_old_brouillon_soon_deleted(dossier).deliver_later + dossier.update_column(:notified_soon_deleted_sent_at, Time.zone.now) + end + end +end diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index aa4fb081d43..ae22e2efd42 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -217,6 +217,26 @@ def notify_transfer end end + def notify_old_brouillon_after_deletion(dossier) + @dossier = dossier + configure_defaults_for_user(dossier.user) + + I18n.with_locale(dossier.user_locale) do + @subject = default_i18n_subject(dossier_id: dossier.id) + mail(to: dossier.user_email_for(:notification), subject: @subject) + end + end + + def notify_old_brouillon_soon_deleted(dossier) + @dossier = dossier + configure_defaults_for_user(dossier.user) + + I18n.with_locale(dossier.user_locale) do + @subject = default_i18n_subject(dossier_id: dossier.id) + mail(to: dossier.user_email_for(:notification), subject: @subject) + end + end + def self.critical_email?(action_name) false end diff --git a/app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml b/app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml new file mode 100644 index 00000000000..4bfa142b450 --- /dev/null +++ b/app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml @@ -0,0 +1,9 @@ +- content_for(:title, "#{@subject}") + +%p= t(:hello, scope: [:views, :shared, :greetings]) + +%p= t('.body', dossier_id: @dossier.id, libelle_demarche: @dossier.procedure.libelle) + +%p= link_to t('.access_dossier'), dossier_url(@dossier), target: '_blank' + += render partial: "layouts/mailers/signature" diff --git a/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml b/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml new file mode 100644 index 00000000000..b9a819c6b04 --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml @@ -0,0 +1,11 @@ +en: + dossier_mailer: + notify_old_brouillon_soon_deleted: + subject: 'Your draft file n°%{dossier_id} will soon be deleted' + body: | + Your file n° %{dossier_id} for "%{libelle_demarche}" has not been modified for more than 3 months. + + It will be automatically deleted in 2 weeks. + + If you wish to keep this application, please update it by logging into your personal space. + access_dossier: 'Access my dossier' diff --git a/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml b/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml new file mode 100644 index 00000000000..0154b1470a4 --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml @@ -0,0 +1,11 @@ +fr: + dossier_mailer: + notify_old_brouillon_soon_deleted: + subject: 'Votre dossier n°%{dossier_id} en brouillon va bientôt être supprimé' + body: | + Votre dossier n° %{dossier_id} pour la démarche "%{libelle_demarche}" n'a pas été modifié depuis plus de 3 mois. + + Il sera automatiquement supprimé dans 2 semaines. + + Si vous souhaitez conserver ce dossier, nous vous invitons à le mettre à jour en vous connectant à votre espace personnel. + access_dossier: 'Accéder à mon dossier' diff --git a/db/migrate/20241126145420_add_notified_soon_deleted_sent_at_to_dossiers.rb b/db/migrate/20241126145420_add_notified_soon_deleted_sent_at_to_dossiers.rb new file mode 100644 index 00000000000..60ea924e7ee --- /dev/null +++ b/db/migrate/20241126145420_add_notified_soon_deleted_sent_at_to_dossiers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddNotifiedSoonDeletedSentAtToDossiers < ActiveRecord::Migration[6.1] + def change + add_column :dossiers, :notified_soon_deleted_sent_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 8da54cc8450..756cedcaf9a 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.0].define(version: 2024_11_12_090128) do +ActiveRecord::Schema[7.0].define(version: 2024_11_26_145420) do # These are extensions that must be enabled in order to support this database enable_extension "pg_buffercache" enable_extension "pg_stat_statements" @@ -505,6 +505,7 @@ t.string "mandataire_first_name" t.string "mandataire_last_name" t.text "motivation" + t.datetime "notified_soon_deleted_sent_at", precision: nil t.bigint "parent_dossier_id" t.string "prefill_token" t.boolean "prefilled" diff --git a/spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb b/spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb new file mode 100644 index 00000000000..b57f87a3022 --- /dev/null +++ b/spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe Cron::NotifyOldBrouillonDossiersSoonDeletedJob, type: :job do + let(:procedure) { create(:procedure) } + + let!(:recent_brouillon) { create(:dossier, :brouillon, procedure: procedure, updated_at: 2.months.ago) } + let!(:old_brouillon) { create(:dossier, :brouillon, procedure: procedure, updated_at: 4.months.ago) } + let!(:old_en_construction) { create(:dossier, :en_construction, procedure: procedure, updated_at: 4.months.ago) } + + subject(:perform_job) { described_class.perform_now } + + describe '#perform' do + before do + allow(DossierMailer).to receive(:notify_old_brouillon_soon_deleted).and_return(double(deliver_later: true)) + perform_job + end + + it 'sends email only for old brouillon dossiers' do + expect(DossierMailer).to have_received(:notify_old_brouillon_soon_deleted).with(old_brouillon).once + expect(DossierMailer).not_to have_received(:notify_old_brouillon_soon_deleted).with(recent_brouillon) + expect(DossierMailer).not_to have_received(:notify_old_brouillon_soon_deleted).with(old_en_construction) + end + end +end diff --git a/spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb b/spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb new file mode 100644 index 00000000000..ddb3c3beab4 --- /dev/null +++ b/spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe Cron::NotifyOldBrouillonDossiersSoonDeletedJob, type: :job do + describe "#perform" do + let(:job) { described_class.new } + + context "when there are old draft dossiers" do + let!(:old_draft_never_notified) { travel_to(4.months.ago) { create(:dossier, :brouillon) } } + let!(:old_draft_notified_before_update) do + travel_to(4.months.ago) do + create(:dossier, :brouillon, notified_soon_deleted_sent_at: 1.month.ago) + end + end + let!(:old_draft_recently_notified) do + travel_to(4.months.ago) do + create(:dossier, :brouillon, notified_soon_deleted_sent_at: 3.months.from_now) + end + end + let!(:recent_draft) { travel_to(2.months.ago) { create(:dossier, :brouillon) } } + let!(:old_non_draft) { travel_to(4.months.ago) { create(:dossier, :en_construction) } } + + it "sends notifications only for eligible draft dossiers" do + expect(DossierMailer).to receive(:notify_old_brouillon_soon_deleted) + .with(old_draft_never_notified) + .and_return(double(deliver_later: true)) + .once + + expect(DossierMailer).to receive(:notify_old_brouillon_soon_deleted) + .with(old_draft_notified_before_update) + .and_return(double(deliver_later: true)) + .once + + expect(DossierMailer).not_to receive(:notify_old_brouillon_soon_deleted) + .with(old_draft_recently_notified) + + job.perform + + expect(old_draft_never_notified.reload.notified_soon_deleted_sent_at).to be_present + expect(old_draft_notified_before_update.reload.notified_soon_deleted_sent_at).to be_present + end + end + + context "when there are no old draft dossiers" do + let!(:recent_draft) { create(:dossier, :brouillon, updated_at: 2.months.ago) } + + it "doesn't send any notifications" do + expect(DossierMailer).not_to receive(:notify_old_brouillon_soon_deleted) + + job.perform + end + end + end +end diff --git a/spec/mailers/dossier_mailer_spec.rb b/spec/mailers/dossier_mailer_spec.rb index f429220f415..8dc8f34b183 100644 --- a/spec/mailers/dossier_mailer_spec.rb +++ b/spec/mailers/dossier_mailer_spec.rb @@ -382,4 +382,72 @@ def notify_deletion_to_administration(hidden_dossier, to_email) end end end + + describe '.notify_old_brouillon_soon_deleted' do + let(:procedure) { create(:procedure, libelle: 'Une superbe démarche') } + let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } + + subject { described_class.notify_old_brouillon_soon_deleted(dossier) } + + it 'renders the subject' do + expect(subject.subject).to eq("Votre dossier n°#{dossier.id} en brouillon va bientôt être supprimé") + end + + it 'renders the receiver email' do + expect(subject.to).to eq([dossier.user.email]) + end + + it 'includes dossier information in body' do + expect(subject.body).to include(dossier.id.to_s) + expect(subject.body).to include(dossier.procedure.libelle) + end + + it 'includes the dossier URL' do + expect(subject.body).to include(dossier_url(dossier, host: ENV.fetch("APP_HOST_LEGACY"))) + end + + context 'with a different locale' do + let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } + before { dossier.user.update(locale: :en) } + + it 'renders in the user locale' do + expect(subject.body).to include('Access my dossier') + end + end + end + + describe '.notify_old_brouillon_after_deletion' do + let(:procedure) { create(:procedure, libelle: 'Une superbe démarche') } + let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } + + subject { described_class.notify_old_brouillon_after_deletion(dossier) } + + it 'renders the subject' do + expect(subject.subject).to eq("Votre dossier n°#{dossier.id} en brouillon a été supprimé pour cause d'inactivité") + end + + it 'renders the receiver email' do + expect(subject.to).to eq([dossier.user.email]) + end + + it 'includes dossier information in body' do + expect(subject.body).to include(dossier.id.to_s) + expect(subject.body).to include(dossier.procedure.libelle) + end + + it 'includes link to create new dossier' do + expect(subject.body).to include(commencer_url(dossier.procedure, host: ENV.fetch("APP_HOST_LEGACY"))) + end + + context 'with a different locale' do + let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } + before { dossier.user.update(locale: :en) } + + it 'renders in the user locale' do + expect(subject.subject).to include("has been deleted due to inactivity") + expect(subject.body).to include("has been automatically deleted") + expect(subject.body).to include("submit a new application") + end + end + end end From 5ab0899a4924f6fc9745b0aa71d229223ee46e6d Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 19 Nov 2024 16:55:23 +0100 Subject: [PATCH 2/3] [#10951] Create a cron job to warn user that old brouillon is deleted --- .../cron/purge_old_brouillon_dossiers_job.rb | 15 +++++++ app/models/dossier.rb | 2 +- ...ify_old_brouillon_after_deletion.html.haml | 9 ++++ .../en.yml | 8 ++++ .../fr.yml | 8 ++++ .../purge_old_brouillon_dossiers_job_spec.rb | 43 +++++++++++++++++++ 6 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 app/jobs/cron/purge_old_brouillon_dossiers_job.rb create mode 100644 app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml create mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml create mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml create mode 100644 spec/jobs/cron/purge_old_brouillon_dossiers_job_spec.rb diff --git a/app/jobs/cron/purge_old_brouillon_dossiers_job.rb b/app/jobs/cron/purge_old_brouillon_dossiers_job.rb new file mode 100644 index 00000000000..40aa4207f1f --- /dev/null +++ b/app/jobs/cron/purge_old_brouillon_dossiers_job.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Cron::PurgeOldBrouillonDossiersJob < Cron::CronJob + self.schedule_expression = "every day at 08:30" + + def perform + Dossier + .state_brouillon + .where(updated_at: ..(3.months + 2.weeks).ago) + .find_each do |dossier| + dossier.hide_and_keep_track!(:automatic, :not_modified_for_a_long_time) + DossierMailer.notify_old_brouillon_after_deletion(dossier).deliver_later + end + end +end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 35531a4b91a..c6edd192a9f 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -567,7 +567,7 @@ def can_be_deleted_by_administration?(reason) end def can_be_deleted_by_automatic?(reason) - reason == :expired && !en_instruction? + reason == :expired && !en_instruction? || reason == :not_modified_for_a_long_time && brouillon? end def can_terminer_automatiquement_by_sva_svr? diff --git a/app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml b/app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml new file mode 100644 index 00000000000..2d832bf03f7 --- /dev/null +++ b/app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml @@ -0,0 +1,9 @@ +- content_for(:title, "#{@subject}") + +%p= t(:hello, scope: [:views, :shared, :greetings]) + +%p= t('.body', dossier_id: @dossier.id, libelle_demarche: @dossier.procedure.libelle) + +%p= t('.new_dossier_html', link: commencer_url(@dossier.procedure)) + += render partial: "layouts/mailers/signature" diff --git a/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml b/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml new file mode 100644 index 00000000000..2d7eb5dd766 --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml @@ -0,0 +1,8 @@ +en: + dossier_mailer: + notify_old_brouillon_after_deletion: + subject: 'Your draft application n°%{dossier_id} has been deleted due to inactivity' + body: | + Your application n° %{dossier_id} for "%{libelle_demarche}" has been automatically deleted as it had not been modified for more than 3 months. + new_dossier_html: | + If you wish to submit a new application for this procedure, you can click here. diff --git a/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml b/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml new file mode 100644 index 00000000000..5cb548d75a9 --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml @@ -0,0 +1,8 @@ +fr: + dossier_mailer: + notify_old_brouillon_after_deletion: + subject: "Votre dossier n°%{dossier_id} en brouillon a été supprimé pour cause d'inactivité" + body: | + Votre dossier n° %{dossier_id} pour la démarche "%{libelle_demarche}" n'ayant pas été modifié depuis plus de 3 mois a été supprimé automatiquement. + new_dossier_html: | + Si vous souhaitez déposer un nouveau dossier pour cette démarche, vous pouvez cliquer ici. diff --git a/spec/jobs/cron/purge_old_brouillon_dossiers_job_spec.rb b/spec/jobs/cron/purge_old_brouillon_dossiers_job_spec.rb new file mode 100644 index 00000000000..7d5edbb9026 --- /dev/null +++ b/spec/jobs/cron/purge_old_brouillon_dossiers_job_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.describe Cron::PurgeOldBrouillonDossiersJob, type: :job do + let(:procedure) { create(:procedure) } + + let!(:recent_brouillon) { travel_to(3.months.ago) { create(:dossier, :brouillon, procedure: procedure) } } + let!(:old_brouillon) { travel_to(5.months.ago) { create(:dossier, :brouillon, procedure: procedure) } } + let!(:very_old_brouillon) { travel_to(6.months.ago) { create(:dossier, :brouillon, procedure: procedure) } } + let!(:old_en_construction) { travel_to(5.months.ago) { create(:dossier, :en_construction, procedure: procedure) } } + + subject(:perform_job) { described_class.perform_now } + + describe '#perform' do + before do + allow(DossierMailer).to receive(:notify_old_brouillon_after_deletion).and_return(double(deliver_later: true)) + end + + it 'hides only old brouillon dossiers' do + expect { perform_job }.to change { Dossier.visible_by_user.count }.by(-2) + + expect(Dossier.visible_by_user.pluck(:id)).to match_array([recent_brouillon.id, old_en_construction.id]) + end + + it 'sends notification emails for each hidden dossier' do + perform_job + + expect(DossierMailer).to have_received(:notify_old_brouillon_after_deletion).with(old_brouillon).once + expect(DossierMailer).to have_received(:notify_old_brouillon_after_deletion).with(very_old_brouillon).once + expect(DossierMailer).not_to have_received(:notify_old_brouillon_after_deletion).with(recent_brouillon) + expect(DossierMailer).not_to have_received(:notify_old_brouillon_after_deletion).with(old_en_construction) + end + + it 'sets the correct hidden_by attributes' do + perform_job + + [old_brouillon, very_old_brouillon].each do |dossier| + dossier.reload + expect(dossier.hidden_by_expired_at).to be_present + expect(dossier.hidden_by_reason).to eq("not_modified_for_a_long_time") + end + end + end +end From 39baa5aae1d8c544596acb319eb9a311739a3d43 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Thu, 28 Nov 2024 10:56:24 +0100 Subject: [PATCH 3/3] [#10951] fixes after review --- .../cron/notify_old_brouillon_dossiers_soon_deleted_job.rb | 4 ++-- app/jobs/cron/purge_old_brouillon_dossiers_job.rb | 2 +- app/models/dossier.rb | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb b/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb index 680d56b3d1c..95e54617c94 100644 --- a/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb +++ b/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Cron::NotifyOldBrouillonDossiersSoonDeletedJob < Cron::CronJob - self.schedule_expression = "every day at 9:00" + self.schedule_expression = "every day at 6:00" def perform Dossier @@ -9,7 +9,7 @@ def perform .where(updated_at: ..3.months.ago) .where("notified_soon_deleted_sent_at IS NULL OR notified_soon_deleted_sent_at < updated_at") .find_each do |dossier| - DossierMailer.notify_old_brouillon_soon_deleted(dossier).deliver_later + DossierMailer.notify_old_brouillon_soon_deleted(dossier).deliver_later(wait: rand(0..3.hours)) dossier.update_column(:notified_soon_deleted_sent_at, Time.zone.now) end end diff --git a/app/jobs/cron/purge_old_brouillon_dossiers_job.rb b/app/jobs/cron/purge_old_brouillon_dossiers_job.rb index 40aa4207f1f..147b16a2988 100644 --- a/app/jobs/cron/purge_old_brouillon_dossiers_job.rb +++ b/app/jobs/cron/purge_old_brouillon_dossiers_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Cron::PurgeOldBrouillonDossiersJob < Cron::CronJob - self.schedule_expression = "every day at 08:30" + self.schedule_expression = "every day at 5:30" def perform Dossier diff --git a/app/models/dossier.rb b/app/models/dossier.rb index c6edd192a9f..ae9d12e572f 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -567,7 +567,10 @@ def can_be_deleted_by_administration?(reason) end def can_be_deleted_by_automatic?(reason) - reason == :expired && !en_instruction? || reason == :not_modified_for_a_long_time && brouillon? + return true if reason == :expired && !en_instruction? + return true if reason == :not_modified_for_a_long_time && brouillon? + + false end def can_terminer_automatiquement_by_sva_svr?