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:
commit
ce221f86af
46 changed files with 626 additions and 66 deletions
|
@ -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])
|
||||
|
||||
|
|
|
@ -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é"
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
9
app/jobs/cron/expired_users_deletion_job.rb
Normal file
9
app/jobs/cron/expired_users_deletion_job.rb
Normal 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
|
13
app/jobs/reset_expiring_dossiers_job.rb
Normal file
13
app/jobs/reset_expiring_dossiers_job.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
39
app/services/expired.rb
Normal 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
|
|
@ -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
|
|
@ -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 }
|
65
app/services/expired/users_deletion_service.rb
Normal file
65
app/services/expired/users_deletion_service.rb
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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 l’instruction du dossier."
|
||||
other: "Vous avez <b>deux semaines</b> pour commencer l’instruction des dossiers."
|
||||
one: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour commencer l’instruction du dossier."
|
||||
other: "Vous avez <b>%{remaining_weeks_before_expiration}</b> pour commencer l’instruction 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."
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
24
spec/jobs/cron/expired_users_deletion_job_spec.rb
Normal file
24
spec/jobs/cron/expired_users_deletion_job_spec.rb
Normal 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
|
19
spec/jobs/reset_expiring_dossiers_job_spec.rb
Normal file
19
spec/jobs/reset_expiring_dossiers_job_spec.rb
Normal 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
|
8
spec/lib/tasks/jobs_spec.rb
Normal file
8
spec/lib/tasks/jobs_spec.rb
Normal 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
|
|
@ -152,7 +152,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
it { expect(subject.body).to include("N° #{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 l’instruction du dossier.") }
|
||||
it { expect(subject.body).to include("Vous avez <b>14 jours</b> pour commencer l’instruction du dossier.") }
|
||||
end
|
||||
|
||||
describe 'termine' do
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
217
spec/services/expired/expired_users_deletion_service_spec.rb
Normal file
217
spec/services/expired/expired_users_deletion_service_spec.rb
Normal 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
|
|
@ -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
|
Loading…
Reference in a new issue