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 000000000..95e54617c
--- /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 6: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(wait: rand(0..3.hours))
+ dossier.update_column(:notified_soon_deleted_sent_at, Time.zone.now)
+ end
+ end
+end
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 000000000..147b16a29
--- /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 5: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/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb
index aa4fb081d..ae22e2efd 100644
--- a/app/mailers/dossier_mailer.rb
+++ b/app/mailers/dossier_mailer.rb
@@ -217,6 +217,26 @@ class DossierMailer < ApplicationMailer
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/models/dossier.rb b/app/models/dossier.rb
index 35531a4b9..ae9d12e57 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -567,7 +567,10 @@ class Dossier < ApplicationRecord
end
def can_be_deleted_by_automatic?(reason)
- reason == :expired && !en_instruction?
+ 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?
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 000000000..2d832bf03
--- /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/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 000000000..4bfa142b4
--- /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_after_deletion/en.yml b/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml
new file mode 100644
index 000000000..2d7eb5dd7
--- /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 000000000..5cb548d75
--- /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/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 000000000..b9a819c6b
--- /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 000000000..0154b1470
--- /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 000000000..60ea924e7
--- /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 8da54cc84..756cedcaf 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 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_12_090128) do
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 000000000..b57f87a30
--- /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 000000000..ddb3c3bea
--- /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/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 000000000..7d5edbb90
--- /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
diff --git a/spec/mailers/dossier_mailer_spec.rb b/spec/mailers/dossier_mailer_spec.rb
index f429220f4..8dc8f34b1 100644
--- a/spec/mailers/dossier_mailer_spec.rb
+++ b/spec/mailers/dossier_mailer_spec.rb
@@ -382,4 +382,72 @@ RSpec.describe DossierMailer, type: :mailer do
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