Merge pull request #9372 from colinux/svr

Admin: permet l'activation du SVR
This commit is contained in:
Colin Darie 2023-09-13 12:08:39 +00:00 committed by GitHub
commit f28739d648
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 261 additions and 36 deletions

View file

@ -3,6 +3,8 @@
class Instructeurs::EnConstructionMenuComponent < ApplicationComponent
attr_reader :dossier
delegate :sva_svr_enabled?, to: :"dossier.procedure"
def initialize(dossier:)
@dossier = dossier
end
@ -22,11 +24,11 @@ class Instructeurs::EnConstructionMenuComponent < ApplicationComponent
end
end
def sva?
dossier.procedure.sva?
end
def sva_resume_method
def sva_svr_resume_method
dossier.procedure.sva_svr_configuration.resume
end
def sva_svr_human_decision
dossier.procedure.sva_svr_configuration.human_decision
end
end

View file

@ -18,11 +18,11 @@
%h4= t('.request_correction')
Lusager sera informé que des modifications sont attendues.
- if sva?
- if sva_resume_method == :reset
Le délai du SVA sera réinitialisé lorquil déclarera avoir complété le dossier.
- if sva_svr_enabled?
- if sva_svr_resume_method == :reset
Le délai du #{sva_svr_human_decision} sera réinitialisé lorquil déclarera avoir complété le dossier.
- else
Le délai du SVA reprendra lorsquil déclarera avoir corrigé le dossier.
Le délai du #{sva_svr_human_decision} reprendra lorsquil déclarera avoir corrigé le dossier.
- menu.with_item(class: "inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier:,
@ -36,14 +36,14 @@
title: 'Marquer en attente de corrections',
confirm: 'Envoyer la demande de corrections ?'}
- if sva?
- if sva_svr_enabled?
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'pending_completion');", role: 'menuitem') do
%span.fr-icon.fr-icon-error-warning-line.fr-text-default--warning.fr-mt-1v{ "aria-hidden": "true" }
.dropdown-description
%h4= t('.request_completion')
Lusager sera informé que son dossier est incomplet. Le délai du SVA sera réinitialisé lorque il déclarera avoir complété le dossier.
Lusager sera informé que son dossier est incomplet. Le délai du #{sva_svr_human_decision} sera réinitialisé lorque il déclarera avoir complété le dossier.
- menu.with_item(class: "inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier:,

View file

@ -1,6 +1,6 @@
---
fr:
title: "Silence Vaut Accord"
title: "Silence Vaut Accord ou Rejet"
subtitle: "Accepter ou Refuser un dossier après un délai"
ready: "Configuré"
needs_configuration: "À configurer"

View file

@ -20,7 +20,7 @@ class Procedure::SVASVRFormComponent < ApplicationComponent
[
{ label: t("disabled", scope:), value: "disabled", disabled: form_disabled? },
{ label: t("sva", scope:), value: "sva", hint: t("sva_hint", scope:) },
{ label: t("svr", scope:), value: "svr", hint: t("svr_hint", scope:), disabled: true, after_label: tag.span("Disponible prochainement", class: "fr-badge fr-badge--sm fr-ml-1w") }
{ label: t("svr", scope:), value: "svr", hint: t("svr_hint", scope:) }
]
end

View file

@ -21,8 +21,8 @@ fr:
svr_hint: "Un dossier est automatiquement refusé si aucun ninstructeur ne sest prononcé avant le délai imparti"
resume_buttons:
continue_label: "Reprendre le décompte depuis le moment où il sétait arrêté"
continue_hint: "Exemple: si linstructeur demande des corrections dun dossier complet alors quil reste 10 jours avant la décision automatique, et que le dossier est re-déposé le 15 avril, il sera automatiquement accepté le 25 avril, sauf à ce que linstructeur se prononce dici là ou demande à nouveau des corrections. En revanche si linstructeur demande à compléter le dossier, le délai sera réinitialisé."
continue_hint: "Exemple: si linstructeur demande des corrections dun dossier complet alors quil reste 10 jours avant la décision automatique, et que le dossier est re-déposé le 15 avril, il sera automatiquement accepté le 26 avril, sauf à ce que linstructeur se prononce dici là ou demande à nouveau des corrections. En revanche si linstructeur demande à compléter le dossier, le délai sera réinitialisé."
reset_label: "Réinitialiser le délai"
reset_hint: "Exemple: si le dossier est re-déposé le 15 avril et que le délai est de 2 mois, la décision sera automatiquement prise le 15 juin, sauf à ce que linstructeur se prononce dici là ou demande à nouveau des corrections."
reset_hint: "Exemple: si le dossier est re-déposé le 15 avril et que le délai est de 2 mois, la décision sera automatiquement prise le 16 juin, sauf à ce que linstructeur se prononce dici là ou demande à nouveau des corrections."
notice_new_files_only: "Information : si vous activez cette règle, seuls les nouveaux dossiers déposés y seront soumis."
notice_edit_denied: "Avertissement : le changement ou la désactivation du SVA/SVR est impossible."

View file

@ -96,6 +96,12 @@ class Dossier < ApplicationRecord
processed_at: processed_at)
end
def refuser_automatiquement(processed_at: Time.zone.now, motivation:)
build(state: Dossier.states.fetch(:refuse),
motivation: motivation,
processed_at: processed_at)
end
def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:sans_suite),
instructeur_email: instructeur&.email,
@ -177,6 +183,10 @@ class Dossier < ApplicationRecord
transitions from: :en_instruction, to: :refuse, guard: :can_terminer?
end
event :refuser_automatiquement, after: :after_refuser_automatiquement do
transitions from: :en_instruction, to: :refuse, guard: :can_refuser_automatiquement?
end
event :classer_sans_suite, after: :after_classer_sans_suite do
transitions from: :en_instruction, to: :sans_suite, guard: :can_terminer?
end
@ -526,7 +536,14 @@ class Dossier < ApplicationRecord
def can_accepter_automatiquement?
return false unless can_terminer?
return true if declarative_triggered_at.nil? && procedure.declarative_accepte? && en_construction?
return true if procedure.sva? && sva_svr_decision_triggered_at.nil? && !pending_correction? && (sva_svr_decision_on.today? || sva_svr_decision_on.past?)
return true if procedure.sva? && can_terminer_automatiquement_by_sva_svr?
false
end
def can_refuser_automatiquement?
return false unless can_terminer?
return true if procedure.svr? && can_terminer_automatiquement_by_sva_svr?
false
end
@ -559,6 +576,10 @@ class Dossier < ApplicationRecord
termine? || reason == :procedure_removed
end
def can_terminer_automatiquement_by_sva_svr?
sva_svr_decision_triggered_at.nil? && !pending_correction? && (sva_svr_decision_on.today? || sva_svr_decision_on.past?)
end
def any_etablissement_as_degraded_mode?
return true if etablissement&.as_degraded_mode?
return true if champs_public_all.any? { _1.etablissement&.as_degraded_mode? }
@ -996,7 +1017,7 @@ class Dossier < ApplicationRecord
if procedure.declarative_accepte?
self.en_instruction_at = self.processed_at
self.declarative_triggered_at = self.processed_at
elsif procedure.sva_svr_enabled?
elsif procedure.sva?
self.sva_svr_decision_triggered_at = self.processed_at
end
@ -1040,6 +1061,23 @@ class Dossier < ApplicationRecord
log_dossier_operation(instructeur, :refuser, self)
end
def after_refuser_automatiquement
# Only SVR can refuse automatically
I18n.with_locale(user.locale || I18n.default_locale) do
self.motivation = I18n.t("shared.dossiers.motivation.refused_by_svr")
end
self.processed_at = traitements.refuser_automatiquement(motivation:).processed_at
self.sva_svr_decision_triggered_at = self.processed_at
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_refuse_notification(self).deliver_later
log_automatic_dossier_operation(:refuser, self)
end
def after_classer_sans_suite(h)
instructeur = h[:instructeur]
motivation = h[:motivation]
@ -1090,6 +1128,8 @@ class Dossier < ApplicationRecord
passer_automatiquement_en_instruction!
elsif en_instruction? && procedure.sva? && may_accepter_automatiquement?
accepter_automatiquement!
elsif en_instruction? && procedure.svr? && may_refuser_automatiquement?
refuser_automatiquement!
elsif will_save_change_to_sva_svr_decision_on?
save! # we always want the most up to date decision when there is a pending correction
end
@ -1187,7 +1227,7 @@ class Dossier < ApplicationRecord
['Dernière mise à jour le', :updated_at],
['Déposé le', :depose_at],
['Passé en instruction le', :en_instruction_at],
procedure.sva_svr_enabled? ? ["Date #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]

View file

@ -26,8 +26,6 @@ class ProcedurePresentation < ApplicationRecord
validate :check_filters_max_length
def self_fields
sva_svr_enabled = procedure.sva_svr_enabled?
[
field_hash('self', 'created_at', type: :date),
field_hash('self', 'updated_at', type: :date),
@ -35,8 +33,7 @@ class ProcedurePresentation < ApplicationRecord
field_hash('self', 'en_construction_at', type: :date),
field_hash('self', 'en_instruction_at', type: :date),
field_hash('self', 'processed_at', type: :date),
sva_svr_enabled && field_hash('self', 'sva_svr_decision_on', type: :date),
sva_svr_enabled && field_hash('self', 'sva_svr_decision_before', type: :date, virtual: true),
*sva_svr_fields(for_filters: true),
field_hash('self', 'updated_since', type: :date, virtual: true),
field_hash('self', 'depose_since', type: :date, virtual: true),
field_hash('self', 'en_construction_since', type: :date, virtual: true),
@ -112,15 +109,32 @@ class ProcedurePresentation < ApplicationRecord
end
def displayed_fields_for_headers
array = [
[
field_hash('self', 'id', classname: 'number-col'),
*displayed_fields,
field_hash('self', 'state', classname: 'state-col')
field_hash('self', 'state', classname: 'state-col'),
*sva_svr_fields
]
end
array << field_hash('self', 'sva_svr_decision_on', classname: 'sva-col') if procedure.sva_svr_enabled?
def sva_svr_fields(for_filters: false)
return if !procedure.sva_svr_enabled?
array
i18n_scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
fields = []
fields << field_hash('self', 'sva_svr_decision_on',
type: :date,
label: I18n.t("#{procedure.sva_svr_decision}_decision_on", scope: i18n_scope),
classname: for_filters ? '' : 'sva-col')
if for_filters
fields << field_hash('self', 'sva_svr_decision_before',
label: I18n.t("#{procedure.sva_svr_decision}_decision_before", scope: i18n_scope),
type: :date, virtual: true)
end
fields
end
def sorted_ids(dossiers, count)

View file

@ -11,7 +11,7 @@ class SVASVRConfiguration
UNIT_OPTIONS = ['days', 'weeks', 'months']
RESUME_OPTIONS = ['continue', 'reset']
validates :decision, inclusion: { in: DECISION_OPTIONS.without('svr') }
validates :decision, inclusion: { in: DECISION_OPTIONS }
validates :period, presence: true, numericality: { only_integer: true }, if: -> { enabled? }
validates :unit, presence: true, inclusion: { in: UNIT_OPTIONS }, if: -> { enabled? }
validates :resume, presence: true, inclusion: { in: RESUME_OPTIONS }, if: -> { enabled? }

View file

@ -11,7 +11,7 @@ def maybe_start_new_page(pdf, size)
end
def clean_string(str)
str.tr(' ', ' ') # replace non breaking space, which are invalid in pdf
str&.gsub(/[[:space:]]/, ' ') # replace non breaking space, which are invalid in pdf
end
def text_box(pdf, text, x, width)
@ -209,7 +209,7 @@ def add_message(pdf, message)
end
format_in_2_lines(pdf, "#{sender}, #{try_format_date(message.created_at)}",
ActionView::Base.full_sanitizer.sanitize(message.body))
ActionView::Base.full_sanitizer.sanitize(clean_string(message.body)))
end
def add_avis(pdf, avis)
@ -276,7 +276,7 @@ prawn_document(page_size: "A4") do |pdf|
add_etat_dossier(pdf, @dossier)
if @dossier.motivation.present?
format_in_2_columns(pdf, "Motif de la décision", @dossier.motivation)
format_in_2_columns(pdf, "Motif de la décision", clean_string(@dossier.motivation))
end
add_title(pdf, 'Historique')
add_etats_dossier(pdf, @dossier)

View file

@ -17,8 +17,10 @@ en:
en_construction_since: Submitted since
en_instruction_since: Instructed since
processed_since: Finished since
sva_svr_decision_on: SVA decision date
sva_svr_decision_before: SVA decision date before
sva_decision_on: SVA decision date
sva_decision_before: SVA decision date before
svr_decision_on: SVR decision date
svr_decision_before: SVR decision date before
user:
email: Requester
followers_instructeurs:

View file

@ -17,8 +17,10 @@ fr:
en_construction_since: En construction depuis
en_instruction_since: En instruction depuis
processed_since: Terminé depuis
sva_svr_decision_on: Date décision SVA
sva_svr_decision_before: Date décision SVA avant
sva_decision_on: Date décision SVA
sva_decision_before: Date décision SVA avant
svr_decision_on: Date décision SVR
svr_decision_before: Date décision SVR avant
user:
email: Demandeur
followers_instructeurs:

View file

@ -5,6 +5,8 @@ en:
details_no_name: "The file was submitted by a FranceConnect account."
details: "The file was submitted by the account of %{name}."
details_updated: "The file was submitted by the account of %{name}, authenticated by FranceConnect on %{date}."
motivation:
refused_by_svr: "The service handling your file was unable to process it within the time limit set by the Silence Vaut Rejet law."
header:
expires_at:
brouillon: "Expires on %{date} (%{duree_conservation_totale} months after this file was created)"

View file

@ -5,6 +5,8 @@ fr:
details_no_name: "Le dossier a été déposé par un compte FranceConnect."
details: "Le dossier a été déposé par le compte de %{name}."
details_updated: "Le dossier a été déposé par le compte de %{name}, authentifié par FranceConnect le %{date}."
motivation:
refused_by_svr: "Le service traitant na 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)"

View file

@ -58,6 +58,19 @@ RSpec.describe Instructeurs::EnConstructionMenuComponent, type: :component do
expect(subject).to have_dropdown_item('Demander une correction')
expect(subject).to have_dropdown_item('Demander à compléter')
expect(subject).to have_dropdown_items(count: 4)
expect(subject).to have_text('Le délai du SVA')
end
end
context 'when procedure is svr' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :svr)) }
it 'renders a dropdown' do
expect(subject).to have_dropdown_title('Demander une correction')
expect(subject).to have_dropdown_item('Demander une correction')
expect(subject).to have_dropdown_item('Demander à compléter')
expect(subject).to have_dropdown_items(count: 4)
expect(subject).to have_text('Le délai du SVR')
end
end
end

View file

@ -48,6 +48,42 @@ RSpec.describe ProcedureSVASVRProcessDossierJob, type: :job do
end
end
context 'when procedure is SVR' do
let(:procedure) { create(:procedure, :published, :svr, :for_individual) }
it 'should refuse dossier' do
expect(subject.sva_svr_decision_on).to eq(Date.current)
expect(subject).to be_refuse
expect(subject.processed_at).to within(1.second).of(Time.current)
end
context 'when decision is scheduled in the future' do
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, depose_at: 1.day.ago, sva_svr_decision_on: 2.months.from_now.to_date) }
it 'should not refuses dossier' do
expect { subject }.not_to change { dossier.reload.updated_at }
expect(subject).to be_en_instruction
end
end
context 'when dossier has pending correction / is en_construction' do
before do
travel_to 2.days.ago do # create correction in past so it will be 3 days of delay
dossier.flag_as_pending_correction!(build(:commentaire, dossier: dossier))
end
end
it 'should not refuses dossier' do
subject
expect(dossier).to be_en_construction
end
it 'should update sva_svr_decision_on with corrections delay' do
expect { subject }.to change { dossier.reload.sva_svr_decision_on }.from(Date.current).to(Date.current + 3.days)
end
end
end
context 'when dossier was submitted before sva was enabled' do
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, depose_at: 2.months.ago) }

View file

@ -1103,6 +1103,46 @@ describe Dossier, type: :model do
end
end
describe '#refuser_automatiquement' do
context 'as svr procedure' do
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:procedure) { create(:procedure, :for_individual, :published, :svr) }
let(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, sva_svr_decision_on: Date.current, en_instruction_at: DateTime.new(2021, 5, 1, 12)) }
before {
freeze_time
allow(NotificationMailer).to receive(:send_refuse_notification).and_return(double(deliver_later: true))
}
subject {
dossier.refuser_automatiquement!
dossier.reload
}
it 'refuses dossier automatiquement' do
expect(subject.en_instruction_at).to eq(DateTime.new(2021, 5, 1, 12))
expect(subject.processed_at).to eq(Time.current)
expect(subject.declarative_triggered_at).to be_nil
expect(subject.sva_svr_decision_triggered_at).to eq(Time.current)
expect(subject.motivation).to include("dans le délai imparti")
expect(subject).to be_refuse
expect(last_operation.operation).to eq('refuser')
expect(last_operation.automatic_operation?).to be_truthy
expect(NotificationMailer).to have_received(:send_refuse_notification).with(dossier)
expect(subject.attestation).to be_nil
expect(dossier.commentaires.count).to eq(1)
end
context 'for an user having english locale' do
before { dossier.user.update!(locale: 'en') }
it 'translates the motivation' do
expect(subject.motivation).to include('within the time limit')
end
end
end
end
describe '#passer_en_instruction!' do
let(:dossier) { create(:dossier, :en_construction, en_construction_close_to_expiration_notice_sent_at: Time.zone.now) }
let(:last_operation) { dossier.dossier_operation_logs.last }
@ -1324,6 +1364,60 @@ describe Dossier, type: :model do
end
end
describe '#can_refuser_automatiquement?' do
let(:dossier) { create(:dossier, state: initial_state) }
let(:initial_state) { :en_instruction }
it { expect(dossier.can_refuser_automatiquement?).to be_falsey }
context 'when procedure is sva/svr' do
let(:decision) { :svr }
before do
dossier.procedure.update!(sva_svr: SVASVRConfiguration.new(decision:).attributes)
dossier.update!(sva_svr_decision_on: Date.current)
end
it { expect(dossier.can_refuser_automatiquement?).to be_truthy }
context 'when procedure is svr' do
let(:decision) { :svr }
before do
dossier.procedure.update!(sva_svr: SVASVRConfiguration.new(decision:).attributes)
dossier.update!(sva_svr_decision_on: Date.current)
end
it { expect(dossier.can_refuser_automatiquement?).to be_truthy }
context 'when sva_svr_decision_on is in the future' do
before { dossier.update!(sva_svr_decision_on: 1.day.from_now) }
it { expect(dossier.can_refuser_automatiquement?).to be_falsey }
end
context 'when dossier has pending correction' do
let(:dossier) { create(:dossier, :en_construction) }
let!(:dossier_correction) { create(:dossier_correction, dossier:) }
it { expect(dossier.can_refuser_automatiquement?).to be_falsey }
end
context 'when decision is sva' do
let(:decision) { :sva }
it { expect(dossier.can_refuser_automatiquement?).to be_falsey }
end
context 'when dossier was already processed by svr' do
before { dossier.update!(sva_svr_decision_triggered_at: 1.hour.ago) }
it { expect(dossier.can_refuser_automatiquement?).to be_falsey }
end
end
end
end
describe "can't transition to terminer when etablissement is in degraded mode" do
let(:instructeur) { create(:instructeur) }
let(:motivation) { 'motivation' }
@ -1955,7 +2049,13 @@ describe Dossier, type: :model do
context 'procedure sva' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :sva)) }
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["Date SVA", :sva_svr_decision_on]) }
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["Date décision SVA", :sva_svr_decision_on]) }
end
context 'procedure svr' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :svr)) }
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["Date décision SVR", :sva_svr_decision_on]) }
end
end

View file

@ -116,7 +116,7 @@ describe ProcedurePresentation do
it { is_expected.to include(name_field, surname_field, gender_field) }
end
context 'when the procedure is sva/svr' do
context 'when the procedure is sva' do
let(:procedure) { create(:procedure, :for_individual, :sva) }
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
@ -127,6 +127,18 @@ describe ProcedurePresentation do
it { is_expected.to include(decision_on, decision_before_field) }
end
context 'when the procedure is svr' do
let(:procedure) { create(:procedure, :for_individual, :svr) }
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
let(:decision_on) { { "label" => "Date décision SVR", "table" => "self", "column" => "sva_svr_decision_on", 'classname' => '', 'virtual' => false, "type" => :date, "scope" => '', "value_column" => :value, 'filterable' => true } }
let(:decision_before_field) { { "label" => "Date décision SVR avant", "table" => "self", "column" => "sva_svr_decision_before", 'classname' => '', 'virtual' => true, "type" => :date, "scope" => '', "value_column" => :value, 'filterable' => true } }
subject { procedure_presentation.fields }
it { is_expected.to include(decision_on, decision_before_field) }
end
end
describe "#displayable_fields_for_select" do