Merge pull request #9666 from mfo/US/expire-user

ETQ RSSI : j'aimerais savoir que DS a une politique d'expiration de donnée agressive 💥
This commit is contained in:
mfo 2023-11-17 10:42:25 +00:00 committed by GitHub
commit ce221f86af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 626 additions and 66 deletions

View file

@ -131,7 +131,7 @@ module Administrateurs
end
def create
new_procedure_params = { max_duree_conservation_dossiers_dans_ds: Procedure::NEW_MAX_DUREE_CONSERVATION }
new_procedure_params = { max_duree_conservation_dossiers_dans_ds: Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH }
.merge(procedure_params)
.merge(administrateurs: [current_administrateur])

View file

@ -56,7 +56,7 @@ module Manager
if !user.can_be_deleted?
fail "Impossible de supprimer cet utilisateur. Il a des dossiers en instruction ou il est administrateur."
end
user.delete_and_keep_track_dossiers_also_delete_user(current_super_admin)
user.delete_and_keep_track_dossiers_also_delete_user(current_super_admin, reason: :user_removed)
logger.info("L'utilisateur #{user.id} est supprimé par #{current_super_admin.id}")
flash[:notice] = "L'utilisateur #{user.id} est supprimé"

View file

@ -0,0 +1,12 @@
class Cron::EnableProcedureExpiresWhenTermineEnabledJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
discard_on StandardError
def perform(*args)
return if ENV['ENABLE_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED_JOB_LIMIT'].blank?
Procedure.where(procedure_expires_when_termine_enabled: false)
.limit(ENV['ENABLE_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED_JOB_LIMIT'])
.order(created_at: :desc)
.update_all(procedure_expires_when_termine_enabled: true)
end
end

View file

@ -1,7 +1,7 @@
class Cron::ExpiredDossiersBrouillonDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 10 pm"
self.schedule_expression = Expired.schedule_at(self)
def perform(*args)
ExpiredDossiersDeletionService.new.process_expired_dossiers_brouillon
Expired::DossiersDeletionService.new.process_expired_dossiers_brouillon
end
end

View file

@ -1,7 +1,7 @@
class Cron::ExpiredDossiersEnConstructionDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 3 pm"
self.schedule_expression = Expired.schedule_at(self)
def perform(*args)
ExpiredDossiersDeletionService.new.process_expired_dossiers_en_construction
Expired::DossiersDeletionService.new.process_expired_dossiers_en_construction
end
end

View file

@ -1,7 +1,7 @@
class Cron::ExpiredDossiersTermineDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 7 am"
self.schedule_expression = Expired.schedule_at(self)
def perform(*args)
ExpiredDossiersDeletionService.new.process_expired_dossiers_termine
Expired::DossiersDeletionService.new.process_expired_dossiers_termine
end
end

View file

@ -1,5 +1,5 @@
class Cron::ExpiredPrefilledDossiersDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 3:00"
self.schedule_expression = Expired.schedule_at(self)
def perform
Dossier.prefilled.state_brouillon.where(user_id: nil, updated_at: ..5.days.ago).destroy_all

View file

@ -0,0 +1,9 @@
class Cron::ExpiredUsersDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
discard_on StandardError
def perform(*args)
return if ENV['EXPIRE_USER_DELETION_JOB_LIMIT'].blank?
Expired::UsersDeletionService.process_expired
end
end

View file

@ -0,0 +1,13 @@
class ResetExpiringDossiersJob < ApplicationJob
def perform(procedure)
procedure.dossiers
.where.not(brouillon_close_to_expiration_notice_sent_at: nil)
.or(Dossier.where.not(en_construction_close_to_expiration_notice_sent_at: nil))
.or(Dossier.where.not(termine_close_to_expiration_notice_sent_at: nil))
.in_batches do |relation|
relation.update_all(brouillon_close_to_expiration_notice_sent_at: nil,
en_construction_close_to_expiration_notice_sent_at: nil,
termine_close_to_expiration_notice_sent_at: nil)
end
end
end

View file

@ -67,7 +67,19 @@ class UserMailer < ApplicationMailer
mail(to: administrateur_or_instructeur.email, subject: subject)
end
def notify_inactive_close_to_deletion(user)
@user = user
@subject = "Votre compte sera supprimé dans #{Expired::REMAINING_WEEKS_BEFORE_EXPIRATION} semaines"
mail(to: user.email, subject: @subject)
end
def self.critical_email?(action_name)
['france_connect_merge_confirmation', "new_account_warning", "ask_for_merge", "invite_instructeur"].include?(action_name)
[
'france_connect_merge_confirmation',
"new_account_warning",
"ask_for_merge",
"invite_instructeur"
].include?(action_name)
end
end

View file

@ -12,7 +12,8 @@ class DeletedDossier < ApplicationRecord
user_removed: 'user_removed',
procedure_removed: 'procedure_removed',
expired: 'expired',
instructeur_request: 'instructeur_request'
instructeur_request: 'instructeur_request',
user_expired: 'user_expired'
}
enum state: {

View file

@ -25,8 +25,7 @@ class Dossier < ApplicationRecord
REMAINING_DAYS_BEFORE_CLOSING = 2
INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
REMAINING_WEEKS_BEFORE_EXPIRATION = 2
INTERVAL_BEFORE_EXPIRATION = "#{REMAINING_WEEKS_BEFORE_EXPIRATION} weeks"
INTERVAL_BEFORE_EXPIRATION = "#{Expired::REMAINING_WEEKS_BEFORE_EXPIRATION} weeks"
MONTHS_AFTER_EXPIRATION = 1
DAYS_AFTER_EXPIRATION = 5
INTERVAL_EXPIRATION = "#{MONTHS_AFTER_EXPIRATION} month #{DAYS_AFTER_EXPIRATION} days"
@ -625,7 +624,7 @@ class Dossier < ApplicationRecord
end
def expiration_notification_date
expiration_date_with_extension - REMAINING_WEEKS_BEFORE_EXPIRATION.weeks
expiration_date_with_extension - Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks
end
def close_to_expiration?

View file

@ -19,7 +19,7 @@ class Procedure < ApplicationRecord
default_scope -> { kept }
OLD_MAX_DUREE_CONSERVATION = 36
NEW_MAX_DUREE_CONSERVATION = ENV.fetch('NEW_MAX_DUREE_CONSERVATION') { 12 }.to_i
NEW_MAX_DUREE_CONSERVATION = Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH
MIN_WEIGHT = 350000
@ -262,7 +262,7 @@ class Procedure < ApplicationRecord
numericality: {
only_integer: true,
greater_than_or_equal_to: 1,
less_than_or_equal_to: 60
less_than_or_equal_to: Expired::MAX_DOSSIER_RENTENTION_IN_MONTH
}
validates_with MonAvisEmbedValidator
@ -312,6 +312,8 @@ class Procedure < ApplicationRecord
validate :validate_auto_archive_on_in_the_future, if: :will_save_change_to_auto_archive_on?
before_save :update_juridique_required
after_save :extend_conservation_for_dossiers
after_initialize :ensure_path_exists
before_save :ensure_path_exists
after_create :ensure_defaut_groupe_instructeur
@ -904,6 +906,15 @@ class Procedure < ApplicationRecord
end
end
def extend_conservation_for_dossiers
return if previous_changes.include?(:duree_conservation_dossiers_dans_ds)
before, after = duree_conservation_dossiers_dans_ds_previous_change
return if [before, after].any?(&:nil?)
return if (after - before).negative?
ResetExpiringDossiersJob.perform_later(self)
end
def ensure_defaut_groupe_instructeur
if self.groupe_instructeurs.empty?
gi = groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)

View file

@ -176,7 +176,7 @@ class User < ApplicationRecord
!administrateur? && !instructeur? && !expert?
end
def delete_and_keep_track_dossiers_also_delete_user(super_admin)
def delete_and_keep_track_dossiers_also_delete_user(super_admin, reason:)
if !can_be_deleted?
raise "Cannot delete this user because they are also instructeur, expert or administrateur"
end
@ -185,29 +185,29 @@ class User < ApplicationRecord
# delete invites
Invite.where(dossier: dossiers).destroy_all
delete_and_keep_track_dossiers(super_admin)
delete_and_keep_track_dossiers(super_admin, reason: :user_removed)
destroy!
end
end
def delete_and_keep_track_dossiers(super_admin)
def delete_and_keep_track_dossiers(super_admin, reason:)
transaction do
# delete dossiers brouillon
dossiers.state_brouillon.each do |dossier|
dossier.hide_and_keep_track!(dossier.user, :user_removed)
dossier.hide_and_keep_track!(dossier.user, reason)
end
dossiers.state_brouillon.find_each(&:purge_discarded)
# delete dossiers en_construction
dossiers.state_en_construction.each do |dossier|
dossier.hide_and_keep_track!(dossier.user, :user_removed)
dossier.hide_and_keep_track!(dossier.user, reason)
end
dossiers.state_en_construction.find_each(&:purge_discarded)
# delete dossiers terminé
dossiers.state_termine.each do |dossier|
dossier.hide_and_keep_track!(dossier.user, :user_removed)
dossier.hide_and_keep_track!(dossier.user, reason)
end
dossiers.update_all(deleted_user_email_never_send: email, user_id: nil, dossier_transfer_id: nil)
end

39
app/services/expired.rb Normal file
View file

@ -0,0 +1,39 @@
module Expired
# User is considered inactive after two years of idleness regarding
# when he does not have a dossier en instruction
# or when his users.last_signed_in_at is smaller than two years ago
INACTIVE_USER_RETATION_IN_YEAR = 2
# Dossier are automatically destroyed after a period (it's configured per Procedure)
# a Dossier.en_instruction? is never destroyed
# otherwise, a dossier is considered for expiracy after its last traitement
DEFAULT_DOSSIER_RENTENTION_IN_MONTH = ENV.fetch('NEW_MAX_DUREE_CONSERVATION') { 12 }.to_i
# Administateur can ask for higher dossier rentention
# but we double check if it's a valid usage
MAX_DOSSIER_RENTENTION_IN_MONTH = 60
# User are always reminded two weeks prior expiracy (for their account as well as their dossier)
REMAINING_WEEKS_BEFORE_EXPIRATION = 2
# Expiracy jobs are run daily.
# it send a lot o email, so we spread our jobs through the day
def self.schedule_at(caller)
case caller.name
when 'Cron::ExpiredPrefilledDossiersDeletionJob'
"every day at 3 am"
when 'Cron::ExpiredDossiersTermineDeletionJob'
"every day at 7 am"
when 'Cron::ExpiredDossiersBrouillonDeletionJob'
"every day at 10 pm"
when 'Cron::ExpiredUsersDeletionJob'
"every day at 11 pm"
when 'Cron::ExpiredDossiersEnConstructionDeletionJob'
"every day at 3 pm"
when 'Cron::EnableProcedureExpiresWhenTermineEnabledJob'
"every day at 2 am"
else
raise 'please, check the schedule to avoid too much email at the same time'
end
end
end

View file

@ -1,8 +1,4 @@
class ExpiredDossiersDeletionService
def initialize(rate_limiter: MailRateLimiter.new(limit: 200, window: 10.minutes))
@rate_limiter = rate_limiter
end
class Expired::DossiersDeletionService < Expired::MailRateLimiter
def process_expired_dossiers_brouillon
send_brouillon_expiration_notices
delete_expired_brouillons_and_notify
@ -18,10 +14,6 @@ class ExpiredDossiersDeletionService
delete_expired_termine_and_notify
end
def safe_send_email(mail)
@rate_limiter.send_with_delay(mail)
end
def send_brouillon_expiration_notices
dossiers_close_to_expiration = Dossier
.brouillon_close_to_expiration
@ -36,7 +28,7 @@ class ExpiredDossiersDeletionService
dossiers,
email
)
safe_send_email(mail)
send_with_delay(mail)
end
end
@ -65,7 +57,7 @@ class ExpiredDossiersDeletionService
dossiers_hash,
email
)
safe_send_email(mail)
send_with_delay(mail)
end
end
@ -87,11 +79,11 @@ class ExpiredDossiersDeletionService
user_notifications.each do |(email, dossiers)|
mail = DossierMailer.notify_near_deletion_to_user(dossiers, email)
safe_send_email(mail)
send_with_delay(mail)
end
administration_notifications.each do |(email, dossiers)|
mail = DossierMailer.notify_near_deletion_to_administration(dossiers, email)
safe_send_email(mail)
send_with_delay(mail)
end
end
@ -114,7 +106,7 @@ class ExpiredDossiersDeletionService
DeletedDossier.where(dossier_id: dossier_ids).to_a,
email
)
safe_send_email(mail)
send_with_delay(mail)
end
end
administration_notifications.each do |(email, dossier_ids)|
@ -124,7 +116,7 @@ class ExpiredDossiersDeletionService
DeletedDossier.where(dossier_id: dossier_ids).to_a,
email
)
safe_send_email(mail)
send_with_delay(mail)
end
end
end

View file

@ -1,4 +1,4 @@
class MailRateLimiter
class Expired::MailRateLimiter
attr_reader :delay, :current_window
def send_with_delay(mail)
@ -15,7 +15,7 @@ class MailRateLimiter
private
def initialize(limit:, window:)
def initialize(limit: 200, window: 10.minutes)
@limit = limit
@window = window
@current_window = { started_at: Time.current, sent: 0 }

View file

@ -0,0 +1,65 @@
class Expired::UsersDeletionService < Expired::MailRateLimiter
def process_expired
# we are working on two dataset because we apply two incompatible join on the same query
# inner join on users not having dossier.en_instruction [so we do not destroy users with dossiers.en_instruction]
# outer join on users not having dossier at all [so we destroy users without dossiers]
[expired_users_without_dossiers, expired_users_with_dossiers].each do |expired_segment|
delete_notified_users(expired_segment)
send_inactive_close_to_expiration_notice(expired_segment)
end
end
private
def send_inactive_close_to_expiration_notice(users)
to_notify_only(users).in_batches do |batch|
batch.each do |user|
send_with_delay(UserMailer.notify_inactive_close_to_deletion(user))
end
batch.update_all(inactive_close_to_expiration_notice_sent_at: Time.zone.now.utc)
end
end
def delete_notified_users(users)
only_notified(users).find_each do |user|
begin
user.delete_and_keep_track_dossiers_also_delete_user(nil, reason: :user_expired)
rescue => e
Sentry.capture_exception(e, extra: { user_id: user.id })
end
end
end
# rubocop:disable DS/Unscoped
def expired_users_with_dossiers
expired_users
.joins(:dossiers)
.group("users.id")
.having("NOT 'en_instruction' = ANY(ARRAY_AGG(dossiers.state))")
end
def expired_users_without_dossiers
expired_users.where.missing(:dossiers)
end
def expired_users
User.unscoped
.where.missing(:expert, :instructeur, :administrateur)
.where(last_sign_in_at: ..Expired::INACTIVE_USER_RETATION_IN_YEAR.years.ago)
end
# rubocop:enable DS/Unscoped
def to_notify_only(users)
users.where(inactive_close_to_expiration_notice_sent_at: nil)
.limit(daily_limit) # ensure to not send too much email
end
def only_notified(users)
users.where.not(inactive_close_to_expiration_notice_sent_at: Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks.ago..)
.limit(daily_limit) # event if we do not send email, avoid to destroy 800k user in one batch
end
def daily_limit
(ENV['EXPIRE_USER_DELETION_JOB_LIMIT'] || 10_000).to_i
end
end

View file

@ -19,7 +19,7 @@
- c.with_body do
%p
= t(:notice, scope: [:administrateurs, :duree_conservation_dossiers_dans_ds])
- if f.object.duree_conservation_dossiers_dans_ds.to_i < Procedure::NEW_MAX_DUREE_CONSERVATION
- if f.object.duree_conservation_dossiers_dans_ds.to_i < Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH
%p
= t(:new_duration_constraint, scope: [:administrateurs, :duree_conservation_dossiers_dans_ds], new_duration_in_month: f.object.max_duree_conservation_dossiers_dans_ds)

View file

@ -12,6 +12,6 @@
%strong= t('.account_active', count: @deleted_dossiers.size)
- if @state == Dossier.states.fetch(:en_construction)
%p= t('.footer_en_construction', count: @deleted_dossiers.size)
%p= t('.footer_en_construction', count: @deleted_dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks))
= render partial: "layouts/mailers/signature"

View file

@ -14,8 +14,8 @@
%p
- if @state == Dossier.states.fetch(:en_construction)
= sanitize(t('.footer_en_construction', count: @dossiers.size))
= sanitize(t('.footer_en_construction', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
- else
= sanitize(t('.footer_termine', count: @dossiers.size))
= sanitize(t('.footer_termine', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
= render partial: "layouts/mailers/signature"

View file

@ -17,8 +17,8 @@
%p
- if @state == Dossier.states.fetch(:en_construction)
= sanitize(t('.footer_en_construction', count: @dossiers.size))
= sanitize(t('.footer_en_construction', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
- else
= sanitize(t('.footer_termine', count: @dossiers.size, dossiers_url: dossiers_url))
= sanitize(t('.footer_termine', count: @dossiers.size, dossiers_url: dossiers_url, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,24 @@
- content_for(:title, @subject)
%p
Bonjour,
%p
Cela fait plus de deux ans que vous ne vous êtes pas connecté à #{APPLICATION_NAME}.
- if @user.dossiers.not_brouillon.count == 0
Aussi vous n'avez plus de dossier sur la plateforme.
%p
Dans le respect du règlement général sur la protection des données, nous allons
%strong supprimer votre compte d'ici #{distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)}.
- if @user.dossiers.not_brouillon.count > 0
%p
%strong Ne vous en faites pas,
vos dossiers traités sont conservés par l'administration. Aussi, à tout moment vous pourrez re-créer une compte sur notre plateforme.
Au besoin, vous pouvez télécharger vos dossiers en suivant ce lien :
= link_to dossiers_url, dossiers_url
%p Vous souhaitez conserver votre compte et vos dossiers ? Connectez-vous avec vos identifiants et nous conserverons vos données.
= render partial: "layouts/mailers/signature"

View file

@ -253,3 +253,6 @@ BULK_EMAIL_QUEUE="low_priority"
# work in progress about attestation_v2
WEASYPRINT_URL="http://10.33.23.204:5000/pdf"
# Use this env var customize the max number of deleted user per day
EXPIRE_USER_DELETION_JOB_LIMIT=10000

View file

@ -14,8 +14,8 @@ fr:
one: "Le dossier suivant dont le traitement est terminé sera bientôt automatiquement supprimé :"
other: "Les dossiers suivants dont le traitement est terminé seront bientôt automatiquement supprimés :"
footer_en_construction:
one: "Vous avez <b>deux semaines</b> pour commencer linstruction du dossier."
other: "Vous avez <b>deux semaines</b> pour commencer linstruction des dossiers."
one: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour commencer linstruction du dossier."
other: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour commencer linstruction des dossiers."
footer_termine:
one: "Vous avez <b>deux semaines</b> pour archiver le dossier."
other: "Vous avez <b>deux semaines</b> pour archiver les dossiers."
one: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour archiver le dossier."
other: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour archiver les dossiers."

View file

@ -0,0 +1,6 @@
class AddExpiredNotificationSentAtToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :inactive_close_to_expiration_notice_sent_at, :datetime, precision: 6, null: true
end
end

View file

@ -0,0 +1,7 @@
class AddIndexToUserssOnLastSignInAt < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_index :users, :last_sign_in_at, algorithm: :concurrently
end
end

View file

@ -1053,8 +1053,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_113317) do
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.integer "failed_attempts", default: 0, null: false
t.datetime "inactive_close_to_expiration_notice_sent_at"
t.datetime "last_sign_in_at", precision: 6
t.string "last_sign_in_ip"
t.string "locale"
t.datetime "locked_at", precision: 6
t.string "loged_in_with_france_connect", default: "false"
@ -1070,6 +1072,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_113317) do
t.datetime "updated_at", precision: 6
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["last_sign_in_at"], name: "index_users_on_last_sign_in_at"
t.index ["requested_merge_into_id"], name: "index_users_on_requested_merge_into_id"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true

View file

@ -7,7 +7,7 @@ namespace :after_party do
.en_construction_close_to_expiration
.without_en_construction_expiration_notice_sent
ExpiredDossiersDeletionService.send_expiration_notices(dossiers_close_to_expiration, :en_construction_close_to_expiration_notice_sent_at)
Expired::DossiersDeletionService.send_expiration_notices(dossiers_close_to_expiration, :en_construction_close_to_expiration_notice_sent_at)
BATCH_SIZE = 1000

View file

@ -0,0 +1,13 @@
namespace :after_party do
desc 'Deployment task: backfill_procedure_expires_when_termine_enabled_without_dossiers'
task backfill_procedure_expires_when_termine_enabled_without_dossiers: :environment do
puts "Running deploy task 'backfill_procedure_expires_when_termine_enabled_without_dossiers'"
Procedure.where.missing(:dossiers)
.where(procedure_expires_when_termine_enabled: false)
.update_all(procedure_expires_when_termine_enabled: true)
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -15,7 +15,7 @@ namespace :support do
user = User.find_by!(email: user_email)
administration = Administration.find_by!(email: administration_email)
user.delete_and_keep_track_dossiers_also_delete_user(administration)
user.delete_and_keep_track_dossiers_also_delete_user(administration, reason: :user_removed)
user.destroy
end
@ -59,7 +59,7 @@ namespace :support do
# remove all the other dossier from the user side
rake_puts "hide #{user.reload.dossiers.count} dossiers"
user.delete_and_keep_track_dossiers(super_admin)
user.delete_and_keep_track_dossiers(super_admin, reason: :user_removed)
owned_procedures, shared_procedures = user.administrateur
.procedures

View file

@ -442,7 +442,7 @@ describe Administrateurs::ProceduresController, type: :controller do
let(:duree_conservation_dossiers_dans_ds) { 17 }
before do
stub_const("Procedure::NEW_MAX_DUREE_CONSERVATION", 18)
stub_const("Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH", 18)
end
subject { post :create, params: { procedure: procedure_params } }
@ -453,7 +453,7 @@ describe Administrateurs::ProceduresController, type: :controller do
subject
last_procedure = Procedure.last
expect(last_procedure.duree_conservation_dossiers_dans_ds).to eq(duree_conservation_dossiers_dans_ds)
expect(last_procedure.max_duree_conservation_dossiers_dans_ds).to eq(Procedure::NEW_MAX_DUREE_CONSERVATION)
expect(last_procedure.max_duree_conservation_dossiers_dans_ds).to eq(Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH)
end
end

View file

@ -56,6 +56,7 @@ describe FranceConnect::ParticulierController, type: :controller do
let(:fc_user) { create(:user, email: 'associated_user@a.com') }
it { expect { subject }.not_to change { FranceConnectInformation.count } }
it { expect { subject }.to change { fc_user.reload.last_sign_in_at } }
it 'signs in with the fci associated user' do
subject

View file

@ -25,7 +25,7 @@ describe Users::SessionsController, type: :controller do
context 'when the credentials are right' do
it 'signs in' do
subject
expect { subject }.to change { user.reload.last_sign_in_at }
expect(response).to redirect_to(root_path)
expect(controller.current_user).to eq(user)

View file

@ -0,0 +1,26 @@
require 'rails_helper'
describe Cron::EnableProcedureExpiresWhenTermineEnabledJob, type: :job do
subject { described_class.perform_now }
let!(:procedure) { create(:procedure, procedure_expires_when_termine_enabled: false) }
context 'when env[ENABLE_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED_JOB_LIMIT] is present' do
before do
allow(ENV).to receive(:[]).with('ENABLE_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED_JOB_LIMIT').and_return(10)
end
it 'performs' do
expect { subject }.to change { procedure.reload.procedure_expires_when_termine_enabled }.from(false).to(true)
end
it 'fails gracefuly by catching any error (to prevent re-enqueue and sending too much email)' do
expect(Procedure).to receive(:where).and_raise(StandardError)
expect { subject }.not_to raise_error
end
end
context 'when env[ENABLE_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED_JOB_LIMIT] is absent' do
it 'does not perform without limit' do
expect { subject }.not_to change { procedure.reload.procedure_expires_when_termine_enabled }
end
end
end

View file

@ -0,0 +1,24 @@
describe Cron::ExpiredUsersDeletionJob do
subject { described_class.perform_now }
context 'when env[EXPIRE_USER_DELETION_JOB_LIMIT] is present' do
before { expect(ENV).to receive(:[]).with('EXPIRE_USER_DELETION_JOB_LIMIT').and_return('anything') }
it 'calls Expired::UsersDeletionService.process_expired' do
expect(Expired::UsersDeletionService).to receive(:process_expired)
subject
end
it 'fails gracefuly by catching any error (to prevent re-enqueue and sending too much email)' do
expect(Expired::UsersDeletionService).to receive(:process_expired).and_raise(StandardError)
expect { subject }.not_to raise_error
end
end
context 'when env[EXPIRE_USER_DELETION_JOB_LIMIT] is absent' do
it 'does not call Expired::UsersDeletionService.process_expired' do
expect(Expired::UsersDeletionService).not_to receive(:process_expired)
subject
end
end
end

View file

@ -0,0 +1,19 @@
describe ResetExpiringDossiersJob do
subject { described_class.new(procedure).perform_now }
let(:duree_conservation_dossiers_dans_ds) { 2 }
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds:) }
describe '.perform_now' do
it 'resets flags' do
expiring_dossier_brouillon = create(:dossier, :brouillon, procedure: procedure, brouillon_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago)
expiring_dossier_en_construction = create(:dossier, :en_construction, procedure: procedure, en_construction_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago)
expiring_dossier_en_termine = create(:dossier, :accepte, procedure: procedure, termine_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago)
subject
expect(expiring_dossier_brouillon.reload.brouillon_close_to_expiration_notice_sent_at).to eq(nil)
expect(expiring_dossier_en_construction.reload.en_construction_close_to_expiration_notice_sent_at).to eq(nil)
expect(expiring_dossier_en_termine.reload.termine_close_to_expiration_notice_sent_at).to eq(nil)
end
end
end

View file

@ -0,0 +1,8 @@
describe 'jobs' do
describe 'schedule' do
subject { Rake::Task['jobs:schedule'].invoke }
it 'runs' do
expect { subject }.not_to raise_error
end
end
end

View file

@ -152,7 +152,7 @@ RSpec.describe DossierMailer, type: :mailer do
it { expect(subject.body).to include("#{dossier.id} ") }
it { expect(subject.body).to include(dossier.procedure.libelle) }
it { expect(subject.body).to include("PDF") }
it { expect(subject.body).to include("Vous avez <b>deux semaines</b> pour commencer linstruction du dossier.") }
it { expect(subject.body).to include("Vous avez <b>14 jours</b> pour commencer linstruction du dossier.") }
end
describe 'termine' do

View file

@ -29,6 +29,10 @@ class UserMailerPreview < ActionMailer::Preview
UserMailer.invite_gestionnaire(user, 'aedfa0d0', groupe_gestionnaire)
end
def notify_inactive_close_to_deletion
UserMailer.notify_inactive_close_to_deletion(user)
end
private
def user

View file

@ -112,4 +112,19 @@ RSpec.describe UserMailer, type: :mailer do
end
end
end
describe '.notify_inactive_close_to_deletion' do
subject { described_class.notify_inactive_close_to_deletion(user) }
it { expect(subject.to).to eq([user.email]) }
it { expect(subject.body).to include("Cela fait plus de deux ans que vous ne vous êtes pas connecté à #{APPLICATION_NAME}.") }
context 'when perform_later is called' do
let(:custom_queue) { 'low_priority' }
before { ENV['BULK_EMAIL_QUEUE'] = custom_queue }
it 'enqueues email is custom queue for low priority delivery' do
expect { subject.deliver_later }.to have_enqueued_job.on_queue(custom_queue)
end
end
end
end

View file

@ -604,7 +604,7 @@ describe Procedure do
it 'should reset duree_conservation_etendue_par_ds' do
expect(subject.duree_conservation_etendue_par_ds).to eq(false)
expect(subject.duree_conservation_dossiers_dans_ds).to eq(Procedure::NEW_MAX_DUREE_CONSERVATION)
expect(subject.duree_conservation_dossiers_dans_ds).to eq(Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH)
end
it 'should duplicate specific objects with different id' do
@ -1656,6 +1656,43 @@ describe Procedure do
end
end
describe 'extend_conservation_for_dossiers' do
let(:duree_conservation_dossiers_dans_ds) { 2 }
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds:) }
let(:expiring_dossier_brouillon) { create(:dossier, :brouillon, procedure: procedure, brouillon_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago) }
let(:expiring_dossier_en_construction) { create(:dossier, :en_construction, procedure: procedure, en_construction_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago) }
let(:expiring_dossier_en_termine) { create(:dossier, :accepte, procedure: procedure, termine_close_to_expiration_notice_sent_at: duree_conservation_dossiers_dans_ds.months.ago) }
let(:not_expiring_dossie) { create(:dossier, :accepte, procedure: procedure, created_at: duree_conservation_dossiers_dans_ds.months.ago) }
before do
procedure
expiring_dossier_brouillon
expiring_dossier_en_construction
expiring_dossier_en_termine
not_expiring_dossie
end
context 'when duree_conservation_dossiers_dans_ds does not changes' do
it 'does not enqueues any job' do
expect(ResetExpiringDossiersJob).not_to receive(:perform_later)
procedure.update!(libelle: 'does not change duree_conservation_dossiers_dans_ds')
end
end
context 'when duree_conservation_dossiers_dans_ds decreases' do
it 'calls extend_conservation_for_dossiers' do
expect(ResetExpiringDossiersJob).not_to receive(:perform_later)
procedure.update(duree_conservation_dossiers_dans_ds: duree_conservation_dossiers_dans_ds - 1)
end
end
context 'when duree_conservation_dossiers_dans_ds increases' do
it 'calls extend_conservation_for_dossiers' do
expect(ResetExpiringDossiersJob).not_to receive(:perform_later)
procedure.update(duree_conservation_dossiers_dans_ds: duree_conservation_dossiers_dans_ds + 1)
end
end
end
private
def create_dossier_with_pj_of_size(size, procedure)

View file

@ -301,14 +301,14 @@ describe User, type: :model do
describe '#delete_and_keep_track_dossiers_also_delete_user' do
let(:super_admin) { create(:super_admin) }
let(:user) { create(:user) }
let(:reason) { :user_rmoved }
context 'without a dossier with processing strted' do
let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) }
let!(:dossier_brouillon) { create(:dossier, user: user) }
context 'without a discarded dossier' do
it "keep track of dossiers and delete user" do
user.delete_and_keep_track_dossiers_also_delete_user(super_admin)
user.delete_and_keep_track_dossiers_also_delete_user(super_admin, reason:)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
@ -322,7 +322,7 @@ describe User, type: :model do
it "keep track of dossiers and delete user" do
dossier_to_delete.hide_and_keep_track!(user, :user_request)
user.delete_and_keep_track_dossiers_also_delete_user(super_admin)
user.delete_and_keep_track_dossiers_also_delete_user(super_admin, reason:)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
@ -337,7 +337,7 @@ describe User, type: :model do
let!(:dossier_termine) { create(:dossier, :accepte, user: user) }
it "keep track of dossiers and delete user" do
user.delete_and_keep_track_dossiers_also_delete_user(super_admin)
user.delete_and_keep_track_dossiers_also_delete_user(super_admin, reason:)
expect(dossier_en_instruction.reload).to be_present
expect(dossier_en_instruction.user).to be_nil

View file

@ -1,4 +1,4 @@
describe ExpiredDossiersDeletionService do
describe Expired::DossiersDeletionService do
let(:warning_period) { 1.month + 5.days }
let(:conservation_par_defaut) { 3.months }
let(:user) { create(:user) }
@ -6,7 +6,7 @@ describe ExpiredDossiersDeletionService do
let(:procedure) { create(:procedure, :published, procedure_opts) }
let(:procedure_2) { create(:procedure, :published, procedure_opts) }
let(:reference_date) { Date.parse("March 8") }
let(:service) { ExpiredDossiersDeletionService.new }
let(:service) { Expired::DossiersDeletionService.new }
describe '#process_expired_dossiers_brouillon' do
before { Timecop.freeze(reference_date) }
after { Timecop.return }

View file

@ -0,0 +1,217 @@
describe Expired::UsersDeletionService do
let(:last_signed_in_not_expired) { (Expired::INACTIVE_USER_RETATION_IN_YEAR - 1).years.ago }
let(:last_signed_in_expired) { (Expired::INACTIVE_USER_RETATION_IN_YEAR + 1).years.ago }
let(:before_close_to_expiration) { nil }
let(:notified_close_to_expiration) { (Expired::REMAINING_WEEKS_BEFORE_EXPIRATION - 1).weeks.ago }
let(:due_close_to_expiration) { (Expired::REMAINING_WEEKS_BEFORE_EXPIRATION + 1).weeks.ago }
let(:mail_double) do
dbl = double()
expect(dbl).to receive(:deliver_later).with(wait: 0)
dbl
end
before { user && dossier }
describe '#process_expired' do
subject { Expired::UsersDeletionService.new.process_expired }
context 'when user is expirable and have a dossier' do
let(:dossier) { create(:dossier, user:, created_at: last_signed_in_expired) }
context 'when user was not notified' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: before_close_to_expiration) }
it 'update user.inactive_close_to_expiration_notice_sent_at ' do
expect(UserMailer).to receive(:notify_inactive_close_to_deletion).with(user).and_return(mail_double)
expect { subject }
.to change { user.reload.inactive_close_to_expiration_notice_sent_at }
.from(nil).to(anything)
end
end
context 'user has been notified 1 week ago' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: notified_close_to_expiration) }
it 'do nothing' do
expect { subject }.not_to change { Dossier.count }
expect { user.reload }.not_to raise_error
end
end
context 'user has been notified 3 weeks ago' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: due_close_to_expiration) }
it 'destroys user and dossier' do
expect { subject }.to change { Dossier.count }.by(-1)
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'when dossier brouillon' do
let(:dossier) { create(:dossier, :brouillon, user:, created_at: last_signed_in_expired) }
it 'destroys user and dossier' do
expect { subject }.to change { Dossier.count }.by(-1)
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when dossier en_construction' do
let(:dossier) { create(:dossier, :en_construction, user:, created_at: last_signed_in_expired) }
it 'destroys user and dossier' do
expect { subject }.to change { Dossier.count }.by(-1)
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when dossier en_instruction' do
let(:dossier) { create(:dossier, :en_instruction, user:, created_at: last_signed_in_expired) }
it 'does not do anything' do
expect { subject }.not_to change { Dossier.count }
expect { user.reload }.not_to raise_error
end
end
context 'when dossier termine' do
let(:dossier) { create(:dossier, :accepte, user:, created_at: last_signed_in_expired) }
it 'marks dossier as hidden_at due to user_removal and remove user' do
expect { subject }.to change { dossier.reload.hidden_by_user_at }.from(nil).to(anything)
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
context 'when user is expirable but does not have a dossier' do
let(:dossier) { nil }
context 'when user was not notified' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: before_close_to_expiration) }
it 'update user.inactive_close_to_expiration_notice_sent_at ' do
expect(UserMailer).to receive(:notify_inactive_close_to_deletion).with(user).and_return(mail_double)
expect { subject }
.to change { user.reload.inactive_close_to_expiration_notice_sent_at }
.from(nil).to(anything)
end
end
context 'when user has been notified 1 week ago' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: notified_close_to_expiration) }
it 'do nothing' do
expect { subject }.not_to change { Dossier.count }
expect { user.reload }.not_to raise_error
end
end
context 'when user has been notified 3 weeks ago' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired, inactive_close_to_expiration_notice_sent_at: due_close_to_expiration) }
it 'destroys user and dossier' do
subject
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
describe '#expired_users_without_dossiers' do
let(:dossier) { nil }
subject { Expired::UsersDeletionService.new.send(:expired_users_without_dossiers) }
context 'when user last_sign_in_at is 1 year ago and has no dossier' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_not_expired) }
it { is_expected.not_to include(user) }
end
context 'when user last_sign_in_at is 3 year ago and has no dossier' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired) }
it { is_expected.to include(user) }
end
context 'when user is expired and has an expert' do
let(:user) { create(:user, expert: create(:expert), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has an instructeur' do
let(:user) { create(:user, instructeur: create(:instructeur), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has an admin' do
let(:user) { create(:user, administrateur: create(:administrateur), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired but have a dossier' do
let(:user) { create(:user, administrateur: create(:administrateur), last_sign_in_at: last_signed_in_expired) }
let(:dossier) { create(:dossier, :brouillon, user:, created_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
end
describe '#expired_users_with_dossiers' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_expired) }
let(:dossier) { create(:dossier, :brouillon, user:, created_at: last_signed_in_expired) }
subject { Expired::UsersDeletionService.new.send(:expired_users_with_dossiers) }
context 'when user is not expired' do
let(:user) { create(:user, last_sign_in_at: last_signed_in_not_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has a dossier brouillon' do
let(:dossier) { create(:dossier, :brouillon, user:, created_at: last_signed_in_expired) }
it { is_expected.to include(user) }
end
context 'when user is expired and has a many dossier brouillon' do
before do
create(:dossier, :brouillon, user:, created_at: last_signed_in_expired)
create(:dossier, :brouillon, user:, created_at: last_signed_in_expired)
end
it { is_expected.to eq([user]) }
end
context 'when user is expired and has a dossier en_construction' do
let(:dossier) { create(:dossier, :en_construction, user:, created_at: last_signed_in_expired) }
it { is_expected.to include(user) }
end
context 'when user is expired and has a dossier en_instruction' do
let(:dossier) { create(:dossier, :en_instruction, user:, created_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has a dossier en_instruction plus another one brouillon' do
before do
create(:dossier, :en_instruction, user:, created_at: last_signed_in_expired)
create(:dossier, :brouillon, user:, created_at: last_signed_in_expired)
end
it { is_expected.to eq([]) }
end
context 'when user is expired and has a dossier termine' do
let(:dossier) { create(:dossier, :accepte, user:, created_at: last_signed_in_expired) }
it { is_expected.to include(user) }
end
context 'when user is expired and has an expert' do
let(:dossier) { create(:dossier, user:, created_at: last_signed_in_expired) }
let(:user) { create(:user, expert: create(:expert), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has an instructeur' do
let(:dossier) { create(:dossier, user:, created_at: last_signed_in_expired) }
let(:user) { create(:user, instructeur: create(:instructeur), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
context 'when user is expired and has an admin' do
let(:dossier) { create(:dossier, user:, created_at: last_signed_in_expired) }
let(:user) { create(:user, administrateur: create(:administrateur), last_sign_in_at: last_signed_in_expired) }
it { is_expected.not_to include(user) }
end
end
end

View file

@ -1,8 +1,8 @@
describe MailRateLimiter do
describe Expired::MailRateLimiter do
describe 'hits limits' do
let(:limit) { 10 }
let(:window) { 2.seconds }
let(:rate_limiter) { MailRateLimiter.new(limit:, window:) }
let(:rate_limiter) { Expired::MailRateLimiter.new(limit:, window:) }
let(:mail) { DossierMailer.notify_automatic_deletion_to_user([], 'tartampion@france.fr') }
it 'decreases current_window[:limit]' do