From 08fcb8bf7017b9b6edf80ed3757ecbde62e2262a Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 11:18:15 +0100 Subject: [PATCH 01/20] [#10951] Fix expiration date for brouillon should use updated_at as reference --- app/models/dossier.rb | 4 ++-- spec/models/dossier_spec.rb | 8 ++++---- .../expired_dossiers_deletion_service_spec.rb | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 57f9f3b882f..e1e89d7442a 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -288,7 +288,7 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon scope :interval_brouillon_close_to_expiration, -> do state_brouillon .visible_by_user - .where("dossiers.created_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) + .where("dossiers.updated_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) end scope :interval_en_construction_close_to_expiration, -> do state_en_construction @@ -599,7 +599,7 @@ def expirable? def expiration_date_reference if brouillon? - created_at + updated_at elsif en_construction? en_construction_at elsif termine? diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 5a7d12a8c01..e88bc2e271f 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -88,10 +88,10 @@ describe 'brouillon_close_to_expiration' do let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) } let!(:young_dossier) { create(:dossier, :en_construction, procedure: procedure) } - let!(:expiring_dossier) { create(:dossier, created_at: 175.days.ago, procedure: procedure) } - let!(:expiring_dossier_with_notification) { create(:dossier, created_at: 175.days.ago, brouillon_close_to_expiration_notice_sent_at: Time.zone.now, procedure: procedure) } - let!(:just_expired_dossier) { create(:dossier, created_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } - let!(:long_expired_dossier) { create(:dossier, created_at: 1.year.ago, procedure: procedure) } + let!(:expiring_dossier) { create(:dossier, updated_at: 175.days.ago, procedure: procedure) } + let!(:expiring_dossier_with_notification) { create(:dossier, updated_at: 175.days.ago, brouillon_close_to_expiration_notice_sent_at: Time.zone.now, procedure: procedure) } + let!(:just_expired_dossier) { create(:dossier, updated_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } + let!(:long_expired_dossier) { create(:dossier, updated_at: 1.year.ago, procedure: procedure) } subject { Dossier.brouillon_close_to_expiration } diff --git a/spec/services/expired/expired_dossiers_deletion_service_spec.rb b/spec/services/expired/expired_dossiers_deletion_service_spec.rb index e822625aadf..6b7308955b9 100644 --- a/spec/services/expired/expired_dossiers_deletion_service_spec.rb +++ b/spec/services/expired/expired_dossiers_deletion_service_spec.rb @@ -19,10 +19,10 @@ let(:date_not_expired) { today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months } context 'send messages for dossiers expiring soon and delete expired' do - let!(:expired_brouillon) { create(:dossier, procedure: procedure, created_at: date_expired, brouillon_close_to_expiration_notice_sent_at: today - (warning_period + 3.days)) } - let!(:brouillon_close_to_expiration) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration) } - let!(:brouillon_close_but_with_notice_sent) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration, brouillon_close_to_expiration_notice_sent_at: Time.zone.now) } - let!(:valid_brouillon) { create(:dossier, procedure: procedure, created_at: date_not_expired) } + let!(:expired_brouillon) { create(:dossier, procedure: procedure, updated_at: date_expired, brouillon_close_to_expiration_notice_sent_at: today - (warning_period + 3.days)) } + let!(:brouillon_close_to_expiration) { create(:dossier, procedure: procedure, updated_at: date_close_to_expiration) } + let!(:brouillon_close_but_with_notice_sent) { create(:dossier, procedure: procedure, updated_at: date_close_to_expiration, brouillon_close_to_expiration_notice_sent_at: Time.zone.now) } + let!(:valid_brouillon) { create(:dossier, procedure: procedure, updated_at: date_not_expired) } before do allow(DossierMailer).to receive(:notify_brouillon_near_deletion).and_call_original @@ -58,19 +58,19 @@ end context 'with a single dossier' do - let!(:dossier) { create(:dossier, procedure: procedure, created_at: created_at) } + let!(:dossier) { create(:dossier, procedure: procedure, updated_at: updated_at) } before { service.send_brouillon_expiration_notices } context 'when the dossier is not close to expiration' do - let(:created_at) { (conservation_par_defaut - 2.weeks - 1.day).ago } + let(:updated_at) { (conservation_par_defaut - 2.weeks - 1.day).ago } it { expect(dossier.reload.brouillon_close_to_expiration_notice_sent_at).to be_nil } it { expect(DossierMailer).not_to have_received(:notify_brouillon_near_deletion) } end context 'when the dossier is close to expiration' do - let(:created_at) { (conservation_par_defaut - 2.weeks + 1.day).ago } + let(:updated_at) { (conservation_par_defaut - 2.weeks + 1.day).ago } it { expect(dossier.reload.brouillon_close_to_expiration_notice_sent_at).not_to be_nil } it { expect(DossierMailer).to have_received(:notify_brouillon_near_deletion).once } @@ -79,8 +79,8 @@ end context 'with 2 dossiers to notice' do - let!(:dossier_1) { create(:dossier, procedure: procedure, user: user, created_at: (conservation_par_defaut - 2.weeks + 1.day).ago) } - let!(:dossier_2) { create(:dossier, procedure: procedure_2, user: user, created_at: (conservation_par_defaut - 2.weeks + 1.day).ago) } + let!(:dossier_1) { create(:dossier, procedure: procedure, user: user, updated_at: (conservation_par_defaut - 2.weeks + 1.day).ago) } + let!(:dossier_2) { create(:dossier, procedure: procedure_2, user: user, updated_at: (conservation_par_defaut - 2.weeks + 1.day).ago) } before { service.send_brouillon_expiration_notices } From f5dae2bae0dcfdaf9501429b0c72464af7508b34 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 16:04:58 +0100 Subject: [PATCH 02/20] [#10951] A brouillon is expired when not updated during 3 months at maximum --- app/models/dossier.rb | 4 ++-- app/services/expired.rb | 3 +++ spec/models/dossier_spec.rb | 4 ++-- .../expired/expired_dossiers_deletion_service_spec.rb | 1 + spec/system/users/brouillon_spec.rb | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index e1e89d7442a..bf3a0c7b168 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -288,7 +288,7 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon scope :interval_brouillon_close_to_expiration, -> do state_brouillon .visible_by_user - .where("dossiers.updated_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) + .where("dossiers.updated_at + dossiers.conservation_extension + (LEAST(procedures.duree_conservation_dossiers_dans_ds, 3) * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) end scope :interval_en_construction_close_to_expiration, -> do state_en_construction @@ -610,7 +610,7 @@ def expiration_date_reference end def expiration_date_with_extension - expiration_date_reference + conservation_extension + procedure.duree_conservation_dossiers_dans_ds.months + expiration_date_reference + duree_totale_conservation_in_months.months end def expiration_notification_date diff --git a/app/services/expired.rb b/app/services/expired.rb index 11d79c76da8..604d247bc57 100644 --- a/app/services/expired.rb +++ b/app/services/expired.rb @@ -18,6 +18,9 @@ module Expired # User are always reminded two weeks prior expiracy (for their account as well as their dossier) REMAINING_WEEKS_BEFORE_EXPIRATION = 2 + # A dossier is considered expired after 3 months max of inactivity + MONTHS_BEFORE_BROUILLON_EXPIRATION = 3 + # Expiracy jobs are run daily. # it send a lot o email, so we spread our jobs through the day def self.schedule_at(caller) diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index e88bc2e271f..e1ab9ff8d09 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -88,8 +88,8 @@ describe 'brouillon_close_to_expiration' do let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) } let!(:young_dossier) { create(:dossier, :en_construction, procedure: procedure) } - let!(:expiring_dossier) { create(:dossier, updated_at: 175.days.ago, procedure: procedure) } - let!(:expiring_dossier_with_notification) { create(:dossier, updated_at: 175.days.ago, brouillon_close_to_expiration_notice_sent_at: Time.zone.now, procedure: procedure) } + let!(:expiring_dossier) { create(:dossier, updated_at: 85.days.ago, procedure: procedure) } + let!(:expiring_dossier_with_notification) { create(:dossier, updated_at: 85.days.ago, brouillon_close_to_expiration_notice_sent_at: Time.zone.now, procedure: procedure) } let!(:just_expired_dossier) { create(:dossier, updated_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } let!(:long_expired_dossier) { create(:dossier, updated_at: 1.year.ago, procedure: procedure) } diff --git a/spec/services/expired/expired_dossiers_deletion_service_spec.rb b/spec/services/expired/expired_dossiers_deletion_service_spec.rb index 6b7308955b9..05212e0ff7f 100644 --- a/spec/services/expired/expired_dossiers_deletion_service_spec.rb +++ b/spec/services/expired/expired_dossiers_deletion_service_spec.rb @@ -9,6 +9,7 @@ let(:procedure_2) { create(:procedure, :published, :new_administrateur, procedure_opts) } let(:reference_date) { Date.parse("March 8") } let(:service) { Expired::DossiersDeletionService.new } + describe '#process_expired_dossiers_brouillon' do before { Timecop.freeze(reference_date) } after { Timecop.return } diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 7e23153ef2e..5b41f219fcc 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -291,7 +291,7 @@ simple_procedure.update(procedure_expires_when_termine_enabled: true) user_old_dossier = create(:dossier, procedure: simple_procedure, - created_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, + updated_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, user: user) login_as(user, scope: :user) visit brouillon_dossier_path(user_old_dossier) From 75914f87e38e81d71d4d798b00358c1f8d6a03a7 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 16:24:16 +0100 Subject: [PATCH 03/20] [#10951] Revert useless code --- .../cron/hide_old_brouillon_dossiers_job.rb | 16 ----- ...old_brouillon_dossiers_soon_deleted_job.rb | 17 ----- app/mailers/dossier_mailer.rb | 20 ------ app/models/dossier.rb | 7 +- ...ify_old_brouillon_after_deletion.html.haml | 9 --- ...otify_old_brouillon_soon_deleted.html.haml | 9 --- .../en.yml | 8 --- .../fr.yml | 8 --- .../notify_old_brouillon_soon_deleted/en.yml | 11 --- .../notify_old_brouillon_soon_deleted/fr.yml | 11 --- .../hide_old_brouillon_dossiers_job_spec.rb | 46 ------------- .../notify_old_brouillon_dossiers_job_spec.rb | 24 ------- ...rouillon_dossiers_soon_deleted_job_spec.rb | 57 ---------------- spec/mailers/dossier_mailer_spec.rb | 68 ------------------- 14 files changed, 2 insertions(+), 309 deletions(-) delete mode 100644 app/jobs/cron/hide_old_brouillon_dossiers_job.rb delete mode 100644 app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb delete mode 100644 app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml delete mode 100644 app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml delete mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml delete mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml delete mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml delete mode 100644 config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml delete mode 100644 spec/jobs/cron/hide_old_brouillon_dossiers_job_spec.rb delete mode 100644 spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb delete mode 100644 spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb diff --git a/app/jobs/cron/hide_old_brouillon_dossiers_job.rb b/app/jobs/cron/hide_old_brouillon_dossiers_job.rb deleted file mode 100644 index 9ffb8702656..00000000000 --- a/app/jobs/cron/hide_old_brouillon_dossiers_job.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Cron::HideOldBrouillonDossiersJob < Cron::CronJob - self.schedule_expression = "every day at 21:00" - - def perform - Dossier - .visible_by_user - .state_brouillon - .where(updated_at: ..(3.months + 2.weeks).ago, notified_soon_deleted_sent_at: ..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/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb b/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb deleted file mode 100644 index 61dc27654cc..00000000000 --- a/app/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class Cron::NotifyOldBrouillonDossiersSoonDeletedJob < Cron::CronJob - self.schedule_expression = "every day at 6:00" - - def perform - Dossier - .visible_by_user - .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/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index ae22e2efd42..aa4fb081d43 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -217,26 +217,6 @@ 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/models/dossier.rb b/app/models/dossier.rb index bf3a0c7b168..6153aa308fe 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Dossier < ApplicationRecord - self.ignored_columns += [:search_terms, :private_search_terms] + self.ignored_columns += [:search_terms, :private_search_terms, :notified_soon_deleted_sent_at] include DossierCloneConcern include DossierCorrectableConcern @@ -568,10 +568,7 @@ def can_be_deleted_by_administration?(reason) end def can_be_deleted_by_automatic?(reason) - return true if reason == :expired && !en_instruction? - return true if reason == :not_modified_for_a_long_time && brouillon? - - false + reason == :expired && !en_instruction? 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 deleted file mode 100644 index 2d832bf03f7..00000000000 --- a/app/views/dossier_mailer/notify_old_brouillon_after_deletion.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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 deleted file mode 100644 index 4bfa142b450..00000000000 --- a/app/views/dossier_mailer/notify_old_brouillon_soon_deleted.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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 deleted file mode 100644 index 2d7eb5dd766..00000000000 --- a/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/en.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 5cb548d75a9..00000000000 --- a/config/locales/views/dossier_mailer/notify_old_brouillon_after_deletion/fr.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b9a819c6b04..00000000000 --- a/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/en.yml +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0154b1470a4..00000000000 --- a/config/locales/views/dossier_mailer/notify_old_brouillon_soon_deleted/fr.yml +++ /dev/null @@ -1,11 +0,0 @@ -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/spec/jobs/cron/hide_old_brouillon_dossiers_job_spec.rb b/spec/jobs/cron/hide_old_brouillon_dossiers_job_spec.rb deleted file mode 100644 index de52344a333..00000000000 --- a/spec/jobs/cron/hide_old_brouillon_dossiers_job_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Cron::HideOldBrouillonDossiersJob, type: :job do - let(:procedure) { create(:procedure) } - - let!(:recent_brouillon) { travel_to(3.months.ago) { create(:dossier, :brouillon, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - let!(:old_brouillon) { travel_to(5.months.ago) { create(:dossier, :brouillon, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - let!(:very_old_brouillon) { travel_to(6.months.ago) { create(:dossier, :brouillon, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - let!(:very_old_brouillon_but_not_notified) { travel_to(6.months.ago) { create(:dossier, :brouillon, procedure: procedure, notified_soon_deleted_sent_at: nil) } } - let!(:old_en_construction) { travel_to(5.months.ago) { create(:dossier, :en_construction, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - let!(:not_visible_dossier) { travel_to(6.months.ago) { create(:dossier, :brouillon, :hidden_by_user, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - let!(:not_visible_dossier2) { travel_to(6.months.ago) { create(:dossier, :brouillon, :hidden_by_expired, procedure: procedure, notified_soon_deleted_sent_at: 3.weeks.ago) } } - - 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) - 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) - expect(DossierMailer).not_to have_received(:notify_old_brouillon_after_deletion).with(not_visible_dossier) - expect(DossierMailer).not_to have_received(:notify_old_brouillon_after_deletion).with(not_visible_dossier2) - 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/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb b/spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb deleted file mode 100644 index b57f87a3022..00000000000 --- a/spec/jobs/cron/notify_old_brouillon_dossiers_job_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index de342cf8a1d..00000000000 --- a/spec/jobs/cron/notify_old_brouillon_dossiers_soon_deleted_job_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# 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) } } - let!(:not_visible_dossier) { travel_to(6.months.ago) { create(:dossier, :brouillon, :hidden_by_user) } } - let!(:not_visible_dossier2) { travel_to(6.months.ago) { create(:dossier, :brouillon, :hidden_by_expired) } } - - 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 - - [old_draft_recently_notified, not_visible_dossier, not_visible_dossier2].each do |dossier| - expect(DossierMailer).not_to receive(:notify_old_brouillon_soon_deleted) - .with(dossier) - end - - 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 b29dde109a0..f456f385537 100644 --- a/spec/mailers/dossier_mailer_spec.rb +++ b/spec/mailers/dossier_mailer_spec.rb @@ -383,72 +383,4 @@ 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 afacd06b95be84331c9048f07e39160382893a1e Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 18:11:03 +0100 Subject: [PATCH 04/20] [#10951] duree_totale_conservation_in_months depends of the dossier state --- app/models/dossier.rb | 10 ++++++---- config/locales/shared.fr.yml | 2 +- spec/system/users/brouillon_spec.rb | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 6153aa308fe..3f02883a626 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -619,6 +619,12 @@ def close_to_expiration? expiration_notification_date < Time.zone.now && Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks.ago < expiration_notification_date end + def duree_totale_conservation_in_months + duree_conservation_dossier = brouillon? ? [procedure.duree_conservation_dossiers_dans_ds, Expired::MONTHS_BEFORE_BROUILLON_EXPIRATION].min : procedure.duree_conservation_dossiers_dans_ds + + duree_conservation_dossier + (conservation_extension / 1.month.to_i) + end + def has_expired? return false if en_instruction? expiration_notification_date < Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks.ago @@ -714,10 +720,6 @@ def text_summary parts.join end - def duree_totale_conservation_in_months - procedure.duree_conservation_dossiers_dans_ds + (conservation_extension / 1.month.to_i) - end - def avis_for_expert(expert) Avis .where(dossier_id: id, confidentiel: false) diff --git a/config/locales/shared.fr.yml b/config/locales/shared.fr.yml index 9683097306f..c576622e9c2 100644 --- a/config/locales/shared.fr.yml +++ b/config/locales/shared.fr.yml @@ -10,7 +10,7 @@ fr: refused_by_svr: "Le service traitant n’a pas été en mesure de traiter votre demande dans le délai imparti par la règle du Silence Vaut Rejet." header: expires_at: - brouillon: "Expirera le %{date} (%{duree_conservation_totale} mois après la création du dossier)" + brouillon: "Expirera le %{date} (%{duree_conservation_totale} mois après la dernière modification du dossier)" en_construction: "Expirera le %{date} (%{duree_conservation_totale} mois après le dépôt du dossier)" en_instruction: "Ce dossier est en instruction, il n’expirera pas" accepte: "Expirera le %{date} (%{duree_conservation_totale} mois après le traitement du dossier)" diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 5b41f219fcc..d5b5c8dc80d 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -181,7 +181,7 @@ { mandatory: false, libelle: "nombre décimal", type: :decimal_number }, { mandatory: false, libelle: 'address', type: :address }, { mandatory: false, libelle: 'IBAN', type: :iban } - ]) + ], duree_conservation_dossiers_dans_ds: 6) } scenario 'save an incomplete dossier as draft but cannot not submit it' do From 14f7ac7c53b91e0a8e8853edeb9a1510476db67e Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 18:48:18 +0100 Subject: [PATCH 05/20] [#10951] fix tests --- spec/system/users/brouillon_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index d5b5c8dc80d..81eaf8538ea 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -289,10 +289,12 @@ scenario 'extends dossier experation date more than one time, ' do simple_procedure.update(procedure_expires_when_termine_enabled: true) - user_old_dossier = create(:dossier, - procedure: simple_procedure, - updated_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, - user: user) + user_old_dossier = travel_to(simple_procedure.duree_conservation_dossiers_dans_ds.month.ago) do + create(:dossier, + procedure: simple_procedure, + updated_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, + user: user) + end login_as(user, scope: :user) visit brouillon_dossier_path(user_old_dossier) @@ -300,7 +302,7 @@ find('#test-user-repousser-expiration').click expect(page).to have_no_selector('#test-user-repousser-expiration') - Timecop.freeze(simple_procedure.duree_conservation_dossiers_dans_ds.month.from_now) do + travel_to((9.months + 1.day).from_now) do visit brouillon_dossier_path(user_old_dossier) expect(page).to have_css('.fr-callout__title', text: 'Votre dossier a expiré', visible: true) find('#test-user-repousser-expiration').click From 6c178e79ee8ec836ae0aa6425db6f4bf165756e1 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 17 Dec 2024 19:42:58 +0100 Subject: [PATCH 06/20] [#10951] Remove test no longer relevant --- spec/controllers/users/dossiers_controller_spec.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 5ca61db084c..4db07dab305 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -1415,19 +1415,6 @@ expect(dossier.reload.hidden_by_user_at).to be_nil end end - - context 'when brouillon has been automatically expired' do - let(:dossier) { create(:dossier, :brouillon, user:) } - - before { - dossier.hide_and_keep_track!(:automatic, :not_modified_for_a_long_time) - } - - it 'must restore hidden attributes' do - expect { subject }.to change { dossier.reload.hidden_by_expired_at }.from(anything).to(nil) - expect(dossier.hidden_by_reason).to eq("not_modified_for_a_long_time") - end - end end describe '#new' do From 0f73cfaf927a55e20e11c81e3d2150f1d2576a47 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Thu, 9 Jan 2025 10:02:04 +0100 Subject: [PATCH 07/20] [#10951] Remove magic number --- app/models/dossier.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 3f02883a626..79fc83e1475 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -286,9 +286,10 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon end scope :interval_brouillon_close_to_expiration, -> do + max_months = 3 state_brouillon .visible_by_user - .where("dossiers.updated_at + dossiers.conservation_extension + (LEAST(procedures.duree_conservation_dossiers_dans_ds, 3) * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) + .where("dossiers.updated_at + dossiers.conservation_extension + (LEAST(procedures.duree_conservation_dossiers_dans_ds, #{max_months}) * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) end scope :interval_en_construction_close_to_expiration, -> do state_en_construction From a5e00da174305c9af9bfcec6580f0d177ba000b5 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Thu, 16 Jan 2025 18:39:32 +0100 Subject: [PATCH 08/20] [#10951] Only process 10K emails max --- .../expired/dossiers_deletion_service.rb | 7 ++- spec/models/dossier_spec.rb | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/services/expired/dossiers_deletion_service.rb b/app/services/expired/dossiers_deletion_service.rb index 62e5d8a76b6..ee278558031 100644 --- a/app/services/expired/dossiers_deletion_service.rb +++ b/app/services/expired/dossiers_deletion_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Expired::DossiersDeletionService < Expired::MailRateLimiter + MAX_BROUILLON_DELETION_EMAILS_TO_PROCESS_PER_DAY = 10000 + def process_expired_dossiers_brouillon send_brouillon_expiration_notices delete_expired_brouillons_and_notify @@ -21,9 +23,7 @@ def send_brouillon_expiration_notices .brouillon_close_to_expiration .without_brouillon_expiration_notice_sent - user_notifications = group_by_user_email(dossiers_close_to_expiration) - - dossiers_close_to_expiration.in_batches.update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now) + user_notifications = group_by_user_email(dossiers_close_to_expiration).take(MAX_BROUILLON_DELETION_EMAILS_TO_PROCESS_PER_DAY) user_notifications.each do |(email, dossiers)| mail = DossierMailer.notify_brouillon_near_deletion( @@ -31,6 +31,7 @@ def send_brouillon_expiration_notices email ) send_with_delay(mail) + Dossier.where(id: dossiers.map(&:id)).update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now) end end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index e1ab9ff8d09..b100c804c69 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -62,6 +62,61 @@ end end end + + describe '.brouillon_expired' do + let(:interval_between_first_and_second_expiration) { Dossier::MONTHS_AFTER_EXPIRATION.months + Dossier::DAYS_AFTER_EXPIRATION.days } + + let!(:dossier_brouillon_expired_and_noticed_long_time_ago) do + travel_to(5.months.ago) do + create(:dossier, + state: :brouillon, + brouillon_close_to_expiration_notice_sent_at: 1.day.ago) + end + end + + let!(:dossier_brouillon_not_expired) do + travel_to(1.month.ago) do + create(:dossier, + state: :brouillon) + end + end + + let!(:dossier_brouillon_expired_but_noticed_recently) do + travel_to(5.months.ago) do + create(:dossier, + state: :brouillon, + brouillon_close_to_expiration_notice_sent_at: (4.months + 20.days).from_now) + end + end + + let!(:dossier_brouillon_expired_but_not_noticed_yet) do + travel_to(5.months.ago) do + create(:dossier, + state: :brouillon) + end + end + + let!(:dossier_instruction_expired) do + travel_to(5.months.ago) do + create(:dossier, + state: :en_instruction, + brouillon_close_to_expiration_notice_sent_at: 1.day.ago) + end + end + + let!(:dossier_hidden) do + travel_to(5.months.ago) do + create(:dossier, + state: :brouillon, + brouillon_close_to_expiration_notice_sent_at: 1.day.ago, + hidden_by_user_at: Time.zone.now) + end + end + + it 'returns only visible brouillon dossiers whose expiration notice period has passed' do + expect(Dossier.brouillon_expired).to contain_exactly(dossier_brouillon_expired_and_noticed_long_time_ago) + end + end end describe 'validations' do From 663be4ed5bca426c32a81d36ad54b11f82bb5f2d Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Thu, 16 Jan 2025 18:53:59 +0100 Subject: [PATCH 09/20] [#10951] Fixes after review --- app/models/dossier.rb | 3 +-- spec/system/users/brouillon_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 79fc83e1475..e65fbd20e82 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -286,10 +286,9 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon end scope :interval_brouillon_close_to_expiration, -> do - max_months = 3 state_brouillon .visible_by_user - .where("dossiers.updated_at + dossiers.conservation_extension + (LEAST(procedures.duree_conservation_dossiers_dans_ds, #{max_months}) * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) + .where("dossiers.updated_at + dossiers.conservation_extension + (LEAST(procedures.duree_conservation_dossiers_dans_ds, #{Expired::MONTHS_BEFORE_BROUILLON_EXPIRATION}) * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION }) end scope :interval_en_construction_close_to_expiration, -> do state_en_construction diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 81eaf8538ea..f766beb9ca0 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -292,7 +292,6 @@ user_old_dossier = travel_to(simple_procedure.duree_conservation_dossiers_dans_ds.month.ago) do create(:dossier, procedure: simple_procedure, - updated_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, user: user) end login_as(user, scope: :user) @@ -302,7 +301,9 @@ find('#test-user-repousser-expiration').click expect(page).to have_no_selector('#test-user-repousser-expiration') - travel_to((9.months + 1.day).from_now) do + months_before_expiration = Expired::MONTHS_BEFORE_BROUILLON_EXPIRATION + simple_procedure.duree_conservation_dossiers_dans_ds + + travel_to((months_before_expiration.months + 1.day).from_now) do visit brouillon_dossier_path(user_old_dossier) expect(page).to have_css('.fr-callout__title', text: 'Votre dossier a expiré', visible: true) find('#test-user-repousser-expiration').click From b59361952c0788f0514c7b166c6b7edaf35cfd49 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 11:29:01 +0100 Subject: [PATCH 10/20] feat(Expired::DossiersDeletionService): limit number of user dossier brouillon close to expiration using `limit` instead of `take` for perf reasons --- app/services/expired/dossiers_deletion_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/expired/dossiers_deletion_service.rb b/app/services/expired/dossiers_deletion_service.rb index ee278558031..4e07545b7ed 100644 --- a/app/services/expired/dossiers_deletion_service.rb +++ b/app/services/expired/dossiers_deletion_service.rb @@ -22,8 +22,9 @@ def send_brouillon_expiration_notices dossiers_close_to_expiration = Dossier .brouillon_close_to_expiration .without_brouillon_expiration_notice_sent + .limit(MAX_BROUILLON_DELETION_EMAILS_TO_PROCESS_PER_DAY) - user_notifications = group_by_user_email(dossiers_close_to_expiration).take(MAX_BROUILLON_DELETION_EMAILS_TO_PROCESS_PER_DAY) + user_notifications = group_by_user_email(dossiers_close_to_expiration) user_notifications.each do |(email, dossiers)| mail = DossierMailer.notify_brouillon_near_deletion( From 22876a6e7973ac1f3cfdbc4cb98be4819983befb Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 11:27:28 +0100 Subject: [PATCH 11/20] feat(Expired::DossiersDeletionService): extend user dossiers brouillon close to expiration windows due to limit which could skip some --- .../expired/dossiers_deletion_service.rb | 15 +++++++++++++-- .../expired_dossiers_deletion_service_spec.rb | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/services/expired/dossiers_deletion_service.rb b/app/services/expired/dossiers_deletion_service.rb index 4e07545b7ed..4a7a7cdbfd2 100644 --- a/app/services/expired/dossiers_deletion_service.rb +++ b/app/services/expired/dossiers_deletion_service.rb @@ -27,12 +27,14 @@ def send_brouillon_expiration_notices user_notifications = group_by_user_email(dossiers_close_to_expiration) user_notifications.each do |(email, dossiers)| + all_user_dossiers = all_user_dossiers_brouillon_close_to_expiration(dossiers.first.user).to_a mail = DossierMailer.notify_brouillon_near_deletion( - dossiers, + all_user_dossiers, email ) + send_with_delay(mail) - Dossier.where(id: dossiers.map(&:id)).update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now) + Dossier.where(id: all_user_dossiers.map(&:id)).update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now) end end @@ -143,4 +145,13 @@ def group_by_fonctionnaire_email(dossiers) end .map { |(email, dossiers)| [email, dossiers.to_a] } end + + def all_user_dossiers_brouillon_close_to_expiration(user) + user.dossiers + .brouillon_close_to_expiration + .without_brouillon_expiration_notice_sent + .visible_by_user + .with_notifiable_procedure(notify_on_closed: true) + .includes(:user, :procedure) + end end diff --git a/spec/services/expired/expired_dossiers_deletion_service_spec.rb b/spec/services/expired/expired_dossiers_deletion_service_spec.rb index 05212e0ff7f..2bb8a77b5b5 100644 --- a/spec/services/expired/expired_dossiers_deletion_service_spec.rb +++ b/spec/services/expired/expired_dossiers_deletion_service_spec.rb @@ -450,4 +450,22 @@ it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).with([dossier_2], dossier_2.procedure.administrateurs.first.email) } end end + + describe 'all_user_dossiers_brouillon_close_to_expiration' do + before { Timecop.freeze(reference_date) } + after { Timecop.return } + + let(:today) { Time.zone.now.at_beginning_of_day } + let(:date_expired) { today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days } + let(:user) { create(:user) } + let!(:expired_brouillon_1) { create(:dossier, procedure:, user:, updated_at: date_expired) } + let!(:expired_brouillon_2) { create(:dossier, procedure:, user:, updated_at: date_expired) } + + it 'find additional dossiers' do + expired_brouillon_1 + expired_brouillon_2 + expect(Expired::DossiersDeletionService.new.send(:all_user_dossiers_brouillon_close_to_expiration, user)) + .to contain_exactly(expired_brouillon_1, expired_brouillon_2) + end + end end From 973324c229e56e2d6c2d412eec314301397876b8 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Fri, 17 Jan 2025 14:49:28 +0100 Subject: [PATCH 12/20] [#10951] Define touch_champs_changed method on dossier to reset brouillon_close_to_expiration_notice_sent_at --- .../champs/piece_justificative_controller.rb | 2 +- app/controllers/users/dossiers_controller.rb | 2 +- app/models/concerns/dossier_clone_concern.rb | 7 +++++-- app/models/dossier.rb | 6 ++++++ spec/models/dossier_spec.rb | 19 +++++++++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/controllers/champs/piece_justificative_controller.rb b/app/controllers/champs/piece_justificative_controller.rb index aaa1052dd91..ed6eeda5117 100644 --- a/app/controllers/champs/piece_justificative_controller.rb +++ b/app/controllers/champs/piece_justificative_controller.rb @@ -32,7 +32,7 @@ def attach_piece_justificative end if save_succeed && dossier.brouillon? - dossier.touch(:last_champ_updated_at, :last_champ_piece_jointe_updated_at) + dossier.touch_champs_changed([:last_champ_updated_at, :last_champ_piece_jointe_updated_at]) end save_succeed diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 39b4bb2a347..f1db2d0fc1a 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -544,7 +544,7 @@ def update_dossier_and_compute_errors # requests it, we ask for field validation errors. if dossier.save if dossier.brouillon? && updated_champs.present? - dossier.touch(:last_champ_updated_at) + dossier.touch_champs_changed([:last_champ_updated_at]) if updated_champs.any?(&:used_by_routing_rules?) @update_contact_information = true RoutingEngine.compute(dossier) diff --git a/app/models/concerns/dossier_clone_concern.rb b/app/models/concerns/dossier_clone_concern.rb index 8ddca734bb5..b538eae2637 100644 --- a/app/models/concerns/dossier_clone_concern.rb +++ b/app/models/concerns/dossier_clone_concern.rb @@ -76,8 +76,11 @@ def merge_fork(editing_fork) rebase! diff = make_diff(editing_fork) apply_diff(diff) - touch(:last_champ_updated_at) - touch(:last_champ_piece_jointe_updated_at) if diff[:updated].any? { |c| c.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) } + + attributes_to_touch = [:last_champ_updated_at] + attributes_to_touch << :last_champ_piece_jointe_updated_at if diff[:updated].any? { |c| c.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) } + + touch_champs_changed(attributes_to_touch) end reload index_search_terms_later diff --git a/app/models/dossier.rb b/app/models/dossier.rb index e65fbd20e82..8186257a7a1 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -1024,6 +1024,12 @@ def termine_and_accuse_lecture? procedure.accuse_lecture? && termine? end + def touch_champs_changed(attributes) + update_columns(attributes.each_with_object({ brouillon_close_to_expiration_notice_sent_at: nil }) do |attribute, hash| + hash[attribute] = Time.zone.now + end) + end + private def build_default_champs diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index b100c804c69..d326a90954d 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -2426,6 +2426,25 @@ it { expect(dossier.sva_svr_decision_in_days).to eq 10 } end + describe '#touch_champs_changed' do + let(:dossier) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: 10.days.ago) } + + subject { dossier.touch_champs_changed(attributes) } + + let(:attributes) { [:last_champ_updated_at] } + + it { is_expected.to change(dossier, :last_champ_updated_at) } + + it { is_expected.to change(dossier, :brouillon_close_to_expiration_notice_sent_at).to(nil) } + + context 'when there is two attributes' do + let(:attributes) { [:last_champ_updated_at, :last_champ_piece_jointe_updated_at] } + + it { is_expected.to change(dossier, :last_champ_updated_at) } + it { is_expected.to change(dossier, :last_champ_piece_jointe_updated_at) } + end + end + private def count_for_month(processed_by_month, month) From fd0b6eca901e5f601f4c61a48b208d39b730eff3 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Fri, 17 Jan 2025 15:01:36 +0100 Subject: [PATCH 13/20] [#10951] Define a test on the dossier controller update action about brouillon_close_to_expiration_notice_sent_at reset --- spec/controllers/users/dossiers_controller_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 4db07dab305..ba762143e0d 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -674,7 +674,7 @@ let(:procedure) { create(:procedure, :published, types_de_champ_public:) } let(:types_de_champ_public) { [{}, { type: :piece_justificative, mandatory: false }] } - let(:dossier) { create(:dossier, user:, procedure:) } + let(:dossier) { create(:dossier, user:, procedure:, brouillon_close_to_expiration_notice_sent_at: 10.days.ago) } let(:first_champ) { dossier.project_champs_public.first } let(:piece_justificative_champ) { dossier.project_champs_public.last } let(:value) { 'beautiful value' } @@ -723,6 +723,7 @@ expect(response).to have_http_status(:ok) expect(dossier.reload.updated_at.year).to eq(2100) expect(dossier.reload.state).to eq(Dossier.states.fetch(:brouillon)) + expect(dossier.reload.brouillon_close_to_expiration_notice_sent_at).to be_nil end end From b3544c4fca740b69f07b03cb9788c00b27106c56 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Fri, 17 Jan 2025 15:17:35 +0100 Subject: [PATCH 14/20] [#10951] fix tests --- spec/models/dossier_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index d326a90954d..bafe0412746 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -2429,7 +2429,7 @@ describe '#touch_champs_changed' do let(:dossier) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: 10.days.ago) } - subject { dossier.touch_champs_changed(attributes) } + subject { -> { dossier.touch_champs_changed(attributes) } } let(:attributes) { [:last_champ_updated_at] } From 6a53fa1183635f807503e5407dc857d77a52b2d8 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 16:33:29 +0100 Subject: [PATCH 15/20] fix(Champs::CarteController): track champ changed from this controller --- app/controllers/champs/carte_controller.rb | 11 +++-- .../champs/carte_controller_spec.rb | 42 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index 6152f4dd73d..c500cfde65a 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -29,13 +29,13 @@ def update if save_feature(geo_area, update_params_feature) head :no_content else - render json: { errors: geo_area.errors.full_messages }, status: :unprocessable_entity + render json: { errors: geo_area.errors.full_message }, status: :unprocessable_entity end end def destroy @champ.geo_areas.find(params[:id]).destroy! - @champ.touch + propagate_touch_champs_changed head :no_content end @@ -78,8 +78,13 @@ def save_feature(geo_area, feature) geo_area.properties.merge!(feature[:properties]) end if geo_area.save - @champ.touch + propagate_touch_champs_changed true end end + + def propagate_touch_champs_changed + @champ.touch + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) + end end diff --git a/spec/controllers/champs/carte_controller_spec.rb b/spec/controllers/champs/carte_controller_spec.rb index 90089d2373f..acbcdcf5e8a 100644 --- a/spec/controllers/champs/carte_controller_spec.rb +++ b/spec/controllers/champs/carte_controller_spec.rb @@ -37,12 +37,13 @@ end describe 'POST #create' do - before do - post :create, params: params - end + subject { post :create, params: params } context 'success' do - it { expect(response.status).to eq 201 } + it do + expect { subject } .to change { dossier.reload.last_champ_updated_at } + expect(response).to have_http_status(:created) + end end context 'error' do @@ -56,7 +57,10 @@ } end - it { expect(response.status).to eq 422 } + it do + expect { subject } .not_to change { dossier.reload.last_champ_updated_at } + expect(response).to have_http_status(:unprocessable_entity) + end end end @@ -70,12 +74,13 @@ } end - before do - patch :update, params: params - end + subject { patch :update, params: params } context 'update geometry' do - it { expect(response.status).to eq 204 } + it do + expect { subject } .to change { dossier.reload.last_champ_updated_at } + expect(response).to have_http_status(:no_content) + end end context 'update description' do @@ -87,16 +92,20 @@ } end - it { + it do + subject expect(response.status).to eq 204 expect(geo_area.reload.description).to eq('un point') - } + end end context 'error' do let(:feature) { attributes_for(:geo_area, :invalid_point) } - it { expect(response.status).to eq 422 } + it do + expect { subject } .not_to change { dossier.reload.last_champ_updated_at } + expect(response).to have_http_status(:unprocessable_entity) + end end end @@ -109,11 +118,12 @@ } end - before do - delete :destroy, params: params - end + subject { delete :destroy, params: params } - it { expect(response.status).to eq 204 } + it do + expect { subject } .to change { dossier.reload.last_champ_updated_at } + expect(response).to have_http_status(:no_content) + end end describe 'GET #index' do From 4cdfaec19feadd867612b9a5b28fdebada2491f9 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 16:45:53 +0100 Subject: [PATCH 16/20] fix(Champs::RepetitionController): track champ repetition changed from this controller --- app/controllers/champs/carte_controller.rb | 5 ----- app/controllers/champs/champ_controller.rb | 5 +++++ .../champs/repetition_controller.rb | 2 ++ .../champs/repetition_controller_spec.rb | 21 ++++++++++++++----- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index c500cfde65a..92ff91415e8 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -82,9 +82,4 @@ def save_feature(geo_area, feature) true end end - - def propagate_touch_champs_changed - @champ.touch - @champ.dossier.touch_champs_changed([:last_champ_updated_at]) - end end diff --git a/app/controllers/champs/champ_controller.rb b/app/controllers/champs/champ_controller.rb index b551009f049..c0d20ceab24 100644 --- a/app/controllers/champs/champ_controller.rb +++ b/app/controllers/champs/champ_controller.rb @@ -23,4 +23,9 @@ def params_row_id def set_champ @champ = find_champ end + + def propagate_touch_champs_changed + @champ.touch + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) + end end diff --git a/app/controllers/champs/repetition_controller.rb b/app/controllers/champs/repetition_controller.rb index 625d98602e3..f2d3e32ccd2 100644 --- a/app/controllers/champs/repetition_controller.rb +++ b/app/controllers/champs/repetition_controller.rb @@ -5,12 +5,14 @@ def add @row_id = @champ.add_row(updated_by: current_user.email) @first_champ_id = @champ.focusable_input_id @row_number = @row_id.nil? ? 0 : @champ.row_ids.find_index(@row_id) + 1 + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) end def remove @champ.remove_row(params[:row_id], updated_by: current_user.email) @to_remove = "safe-row-selector-#{params[:row_id]}" @to_focus = @champ.focusable_input_id || helpers.dom_id(@champ, :create_repetition) + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) end private diff --git a/spec/controllers/champs/repetition_controller_spec.rb b/spec/controllers/champs/repetition_controller_spec.rb index 7a8c41bf55c..ca6ca4b52c4 100644 --- a/spec/controllers/champs/repetition_controller_spec.rb +++ b/spec/controllers/champs/repetition_controller_spec.rb @@ -1,20 +1,31 @@ # frozen_string_literal: true describe Champs::RepetitionController, type: :controller do + before { sign_in dossier.user } + + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, mandatory: true, children: [{ libelle: 'Nom' }, { type: :integer_number, libelle: 'Age' }] }]) } + let(:dossier) { create(:dossier, procedure:) } + let(:repetition) { dossier.project_champs_public.find(&:repetition?) } + describe '#remove' do - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, mandatory: true, children: [{ libelle: 'Nom' }, { type: :integer_number, libelle: 'Age' }] }]) } - let(:dossier) { create(:dossier, procedure:) } - let(:repetition) { dossier.project_champs_public.find(&:repetition?) } let(:row) { dossier.champs.find(&:row?) } - before { sign_in dossier.user } - subject { delete :remove, params: { dossier_id: dossier, stable_id: repetition.stable_id, row_id: row.row_id }, format: :turbo_stream } context 'removes repetition' do it { expect { subject }.not_to change { dossier.reload.champs.size } } it { expect { subject }.to change { dossier.reload; dossier.project_champs_public.find(&:repetition?).row_ids.size }.from(1).to(0) } it { expect { subject }.to change { row.reload.discarded_at }.from(nil).to(Time) } + it { expect { subject }.to change { dossier.reload.last_champ_updated_at } } + end + end + + describe '#add' do + subject { post :add, params: { dossier_id: dossier, stable_id: repetition.stable_id }, format: :turbo_stream } + + context 'add repetition' do + it { expect { subject }.to change { dossier.reload.champs.size } } + it { expect { subject }.to change { dossier.reload.last_champ_updated_at } } end end end From 694c63ebdb637deaa8346ecfc092bef77fdd2ad2 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 16:52:43 +0100 Subject: [PATCH 17/20] fix(Champs::RnaController): track champ rna changed from this controller --- app/controllers/champs/rna_controller.rb | 1 + spec/controllers/champs/rna_controller_spec.rb | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/controllers/champs/rna_controller.rb b/app/controllers/champs/rna_controller.rb index a64bfb7cd6c..78431d67b21 100644 --- a/app/controllers/champs/rna_controller.rb +++ b/app/controllers/champs/rna_controller.rb @@ -8,5 +8,6 @@ def show unless @champ.fetch_association!(rna) @error = @champ.association_fetch_error_key end + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) end end diff --git a/spec/controllers/champs/rna_controller_spec.rb b/spec/controllers/champs/rna_controller_spec.rb index 3f5c59daf79..3ebe8c323da 100644 --- a/spec/controllers/champs/rna_controller_spec.rb +++ b/spec/controllers/champs/rna_controller_spec.rb @@ -38,15 +38,21 @@ let(:status) { 422 } let(:body) { '' } - subject! { get :show, params: params, format: :turbo_stream } + subject { get :show, params: params, format: :turbo_stream } it 'clears the data on the model' do + subject expect(champ.reload.data).to be_nil end it 'clears any information or error message' do + subject expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :rna_info)) end + + it 'updates dossier.last_champs_updated_at' do + expect { subject }.to change { dossier.reload.last_champ_updated_at } + end end context 'when the RNA is invalid' do @@ -54,15 +60,21 @@ let(:status) { 422 } let(:body) { '' } - subject! { get :show, params: params, format: :turbo_stream } + subject { get :show, params: params, format: :turbo_stream } it 'clears the data on the model' do + subject expect(champ.reload.data).to be_nil end it 'displays a “RNA is invalid” error message' do + subject expect(response.body).to include("Le numéro RNA doit commencer par un W majuscule suivi de 9 chiffres ou lettres") end + + it 'updates dossier.last_champs_updated_at' do + expect { subject }.to change { dossier.reload.last_champ_updated_at } + end end context 'when the RNA is unknow' do From ffe4b198ee720797dd790357e0c5e1a6ccf2ff35 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 17 Jan 2025 17:01:14 +0100 Subject: [PATCH 18/20] fix(Champs::SiretController): track champ siret changed from this controller --- app/controllers/champs/siret_controller.rb | 1 + spec/controllers/champs/siret_controller_spec.rb | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/champs/siret_controller.rb b/app/controllers/champs/siret_controller.rb index 03fc72f3267..0f18ed691b4 100644 --- a/app/controllers/champs/siret_controller.rb +++ b/app/controllers/champs/siret_controller.rb @@ -10,5 +10,6 @@ def show else @siret = @champ.etablissement_fetch_error_key end + @champ.dossier.touch_champs_changed([:last_champ_updated_at]) end end diff --git a/spec/controllers/champs/siret_controller_spec.rb b/spec/controllers/champs/siret_controller_spec.rb index fdd483ac1f1..fbdcd9a86d7 100644 --- a/spec/controllers/champs/siret_controller_spec.rb +++ b/spec/controllers/champs/siret_controller_spec.rb @@ -42,15 +42,21 @@ end context 'when the SIRET is empty' do - subject! { get :show, params: params, format: :turbo_stream } + subject { get :show, params: params, format: :turbo_stream } it 'clears the etablissement on the model' do + subject expect(champ.reload.etablissement).to be_nil end it 'clears any information or error message' do + subject expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :siret_info)) end + + it 'updates dossier.last_champ_updated_at' do + expect { subject }.to change { dossier.reload.last_champ_updated_at } + end end context "when the SIRET is invalid because of it's length" do From a730b2b371fd050898217d408d1fbf6f778c0156 Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Fri, 17 Jan 2025 17:34:17 +0100 Subject: [PATCH 19/20] [#10951] fix tests --- app/controllers/champs/carte_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index 92ff91415e8..4c95743418d 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -29,7 +29,7 @@ def update if save_feature(geo_area, update_params_feature) head :no_content else - render json: { errors: geo_area.errors.full_message }, status: :unprocessable_entity + render json: { errors: geo_area.errors.full_messages }, status: :unprocessable_entity end end From 1fe7d42860a577e0333575e819d31eccdb3e1e2a Mon Sep 17 00:00:00 2001 From: Mathieu Magnin Date: Tue, 21 Jan 2025 11:27:49 +0100 Subject: [PATCH 20/20] [#10951] fix tests --- app/models/dossier.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 8186257a7a1..f722d72edc2 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -1025,8 +1025,9 @@ def termine_and_accuse_lecture? end def touch_champs_changed(attributes) - update_columns(attributes.each_with_object({ brouillon_close_to_expiration_notice_sent_at: nil }) do |attribute, hash| - hash[attribute] = Time.zone.now + now = Time.zone.now + update_columns(attributes.each_with_object({ brouillon_close_to_expiration_notice_sent_at: nil, updated_at: now }) do |attribute, hash| + hash[attribute] = now end) end