Merge pull request #9104 from colinux/sva

ETQ admin je peux configurer ma démarche en SVA/SVR
This commit is contained in:
Colin Darie 2023-07-11 08:21:46 +00:00 committed by GitHub
commit 0d106cdf4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 1737 additions and 80 deletions

View file

@ -29,3 +29,15 @@
vertical-align: baseline;
}
}
.badge-group {
display: flex;
.fr-badge {
margin-right: $default-spacer;
}
.fr-badge:last-child {
margin-right: 0;
}
}

View file

@ -1,7 +1,22 @@
class Dsfr::RadioButtonListComponent < ApplicationComponent
def initialize(form:, target:, buttons:)
attr_reader :error
def initialize(form:, target:, buttons:, error: nil)
@form = form
@target = target
@buttons = buttons
@error = error
end
def error?
# TODO: mettre correctement le aria-labelled-by avec l'id du div qui contient les erreurs
# https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bouton-radio/
@error.present?
end
def each_button
@buttons.each do |button|
yield(*button.values_at(:label, :value, :hint), **button.except(:label, :value, :hint))
end
end
end

View file

@ -1,13 +1,19 @@
%fieldset.fr-fieldset{ 'aria-labelledby': 'radio-hint-element-legend radio-hint-element-messages' }
%fieldset{ class: class_names("fr-fieldset": true, "fr-fieldset--error": error?), 'aria-labelledby': 'radio-hint-element-legend radio-hint-element-messages', role: error? ? :group : nil }
%legend.fr-fieldset__legend--regular.fr-fieldset__legend
= content
- @buttons.map { _1.values_at(:label, :value, :hint) }.each do |label, value, hint|
- each_button do |label, value, hint, **button_options|
.fr-fieldset__element
.fr-radio-group
= @form.radio_button @target, value
= @form.radio_button @target, value, **button_options
= @form.label @target, value: value, class: 'fr-label' do
- capture do
= label
%span.fr-hint-text= hint
.fr-messages-group{ 'aria-live': 'assertive' }
= button_options[:after_label] if button_options[:after_label]
%span.fr-hint-text= hint if hint
.fr-messages-group{ 'aria-live': 'assertive' }
- if error?
%p.fr-message.fr-message--error= error

View file

@ -15,10 +15,18 @@ class Instructeurs::EnConstructionMenuComponent < ApplicationComponent
end
def menu_label
if dossier.en_construction?
if !dossier.may_repasser_en_construction?
t('.request_correction')
else
t(".revert_en_construction")
end
end
def sva?
dossier.procedure.sva?
end
def sva_resume_method
dossier.procedure.sva_svr_configuration.resume
end
end

View file

@ -2,3 +2,4 @@
en:
revert_en_construction: Revert to in progress
request_correction: Request a correction
request_completion: Request to complete

View file

@ -2,3 +2,4 @@
fr:
revert_en_construction: Repasser en construction
request_correction: Demander une correction
request_completion: Demander à compléter

View file

@ -16,7 +16,13 @@
.dropdown-description
%h4= t('.request_correction')
Lusager sera informé que des modifications sont attendues
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.
- else
Le délai du SVA 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:,
@ -29,3 +35,24 @@
process_action: nil,
title: 'Marquer en attente de corrections',
confirm: 'Envoyer la demande de corrections ?'}
- if sva?
- 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.
- menu.with_item(class: "inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier:,
visible: false,
form_path: pending_correction_instructeur_dossier_path(dossier.procedure, dossier, kind: :incomplete),
placeholder: 'Expliquez au demandeur comment compléter son dossier',
popup_class: 'pending_completion',
button_justificatif_label: "Ajouter une pièce jointe (facultatif)",
process_button: dossier.en_construction? ? 'Valider' : 'Valider et repasser en construction',
process_action: nil,
title: 'Marquer le dossier comme incomplet',
confirm: 'Envoyer la demande de complétion ?'}

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
class Instructeurs::SVASVRDecisionBadgeComponent < ApplicationComponent
attr_reader :object
attr_reader :procedure
attr_reader :with_label
def initialize(projection_or_dossier:, procedure:, with_label: false)
@object = projection_or_dossier
@procedure = procedure
@decision = procedure.sva_svr_configuration.decision.to_sym
@with_label = with_label
end
def render?
return false unless procedure.sva_svr_enabled?
[:en_construction, :en_instruction].include? object.state.to_sym
end
def without_date?
object.sva_svr_decision_on.nil?
end
def classes
class_names(
'fr-badge fr-badge--sm': true,
'fr-badge--warning': soon?,
'fr-badge--info': !soon?
)
end
def soon?
object.sva_svr_decision_on < 7.days.from_now.to_date
end
def pending_correction?
object.pending_correction?
end
def days_count
(object.sva_svr_decision_on - Date.current).to_i
end
def sva?
@decision == :sva
end
def svr?
@decision == :svr
end
def label_for_badge
sva? ? "SVA :" : "SVR :"
end
end

View file

@ -0,0 +1,10 @@
---
en:
no_sva: Submitted before SVA
no_svr: Submitted before SVR
in_days:
zero: Today
one: Tomorrow
other: in %{count} days
remaining_days_after_correction:
other: "%{count} d. after correction"

View file

@ -0,0 +1,10 @@
---
fr:
no_sva: Déposé avant SVA
no_svr: Déposé avant SVR
in_days:
zero: Aujourdhui
one: Demain
other: dans %{count} jours
remaining_days_after_correction:
other: "%{count} j. après correction"

View file

@ -0,0 +1,11 @@
- if without_date?
%span.fr-badge.fr-badge--sm
= t(sva? ? '.no_sva' : '.no_svr')
- else
%span{ class: classes }
- if with_label.present?
= label_for_badge
- if pending_correction?
= t('.remaining_days_after_correction', count: days_count)
- else
= t('.in_days', count: days_count)

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class Procedure::Card::SVASVRComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
end

View file

@ -0,0 +1,4 @@
---
en:
ready: "Configuré"
needs_configuration: "À configurer"

View file

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

View file

@ -0,0 +1,14 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to edit_admin_procedure_sva_svr_path(@procedure), class: 'fr-tile fr-enlarge-link', id: 'sva' do
.fr-tile__body.flex.justify-between
- if @procedure.sva_svr_enabled?
%div
%span.icon.accept
%p.fr-tile-status-accept= t('.ready')
- else
%div
%span.icon.clock
%p.fr-tile-status-todo= t('.needs_configuration')
%h3.fr-h6.fr-mt-10v= t('.title')
%p.fr-tile-subtitle= t('.subtitle')
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class Procedure::SVASVRFormComponent < ApplicationComponent
attr_reader :procedure, :configuration
def initialize(procedure:, configuration:)
@procedure = procedure
@configuration = configuration
end
def form_disabled?
return false if procedure.brouillon?
procedure.sva_svr_enabled?
end
def decision_buttons
scope = ".decision_buttons"
[
{ 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") }
]
end
def resume_buttons
scope = ".resume_buttons"
[
{
value: "continue",
label: t("continue_label", scope: scope),
hint: t("continue_hint", scope: scope)
},
{
value: "reset",
label: t("reset_label", scope: scope),
hint: t("reset_hint", scope: scope)
}
]
end
end

View file

@ -0,0 +1,28 @@
---
en:
rule: Rule to apply
delay: Configuration of the delay before decision
unit_labels:
months: months
weeks: weeks
days: days
resume_method: How to calculate the delay when the applicant resubmits their corrected file?
resume_intro: |
When an instructor asks for a file to be corrected, the countdown of the delay is interrupted.
The delay resumes when the applicant resubmits their file stating that they have made the requested corrections.
If the file has been declared incomplete, the delay will be reset, regardless of the configuration below.
submit: Apply SVA/SVR configuration
cancel: Cancel
decision_buttons:
disabled: "Disabled"
sva: "Silence Equals Acceptation (SVA)"
sva_hint: "A file is automatically accepted if no instructor has pronounced before the allotted time"
svr: "Silence Equals Rejection (SVR)"
svr_hint: "A file is automatically rejected if no instructor has pronounced before the allotted time"
resume_buttons:
continue_label: "Resume countdown from where it stopped"
continue_hint: "Example: if the instructor requests corrections to a complete file with 10 days to go before the automatic decision, and the file is resubmitted on April 15, it will be automatically accepted on April 25, unless the instructor makes a decision by then or requests corrections again. On the other hand, if the inspector asks for the file to be completed, the deadline will be reset."
reset_label: "Reset the delay"
reset_hint: "Example: if the file is resubmitted on April 15 and the delay is 2 months, the decision will be automatically made on June 15, unless the instructor pronounces in the meantime or asks for corrections again."
notice_new_files_only: "Information: if you activate this rule, only the newly submitted files will be subject to it."
notice_edit_denied: "Warning: SVA/SVR cannot be changed or disabled."

View file

@ -0,0 +1,28 @@
---
fr:
rule: Règle à appliquer
delay: Configuration du délai avant décision
unit_labels:
months: mois
weeks: semaines
days: jours
resume_method: Comment calculer le délai lorsque le demandeur re-dépose son dossier corrigé ?
resume_intro: |
Lorsquun instructeur demande de corriger un dossier, le décompte du délai est interrompu.
Le délai reprend lorsque le demandeur redépose son dossier en déclarant avoir effectué les corrections demandées.
Si le dossier avait été déclaré incomplet, le délai sera réinitialisé, quelle que soit la configuration ci-dessous.
submit: Appliquer la configuration SVA/SVR
cancel: Annuler
decision_buttons:
disabled: "Désactivé"
sva: "Silence Vaut Accord"
sva_hint: "Un dossier est automatiquement accepté si aucun ninstructeur ne sest prononcé avant le délai imparti"
svr: "Silence Vaut Rejet"
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é."
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."
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

@ -0,0 +1,33 @@
= form_for [procedure, configuration], url: admin_procedure_sva_svr_path(procedure), method: :put do |f|
- if procedure.publiee? && !procedure.sva_svr_enabled?
.fr-alert.fr-alert--info.fr-alert--sm.fr-mb-4w
%p= t('.notice_new_files_only')
- if procedure.publiee? && procedure.sva_svr_enabled?
.fr-alert.fr-alert--warning.fr-alert--sm.fr-mb-4w
%p= t('.notice_edit_denied')
%fieldset.fr-fieldset
%legend.fr-fieldset__legend= t(".rule")
= render Dsfr::RadioButtonListComponent.new(form: f, target: :decision, buttons: decision_buttons, error: configuration.errors[:decision].first)
%fieldset.fr-fieldset
%legend.fr-fieldset__legend= t(".delay")
.fr-fieldset__element.fr-fieldset__element--inline
.fr-input-group
= f.number_field :period, class: 'fr-input', disabled: form_disabled?
.fr-fieldset__element.fr-fieldset__element--inline
.fr-select-group
= f.select :unit, options_for_select(SVASVRConfiguration.unit_options.map { [t(_1, scope: ".unit_labels"), _1] }, selected: configuration.unit), {}, class: 'fr-select', disabled: form_disabled?
%fieldset.fr-fieldset
%legend.fr-fieldset__legend
= t(".resume_method")
%span.fr-hint-text
= t(".resume_intro")
= render Dsfr::RadioButtonListComponent.new(form: f, target: :resume, buttons: resume_buttons)
= f.submit t(".submit"), class: "fr-btn", disabled: form_disabled?
= link_to t(".cancel"), admin_procedure_path(procedure.id), class: "fr-btn fr-btn--secondary fr-ml-2w"

View file

@ -0,0 +1,34 @@
module Administrateurs
class SVASVRController < AdministrateurController
before_action :retrieve_procedure
def show
redirect_to edit_admin_procedure_sva_svr_path(@procedure.id)
end
def edit
@configuration = @procedure.sva_svr_configuration
end
def update
@configuration = @procedure.sva_svr_configuration
@configuration.assign_attributes(configuration_params)
if @configuration.valid?
@procedure.update!(sva_svr: @configuration.attributes)
flash.notice = "La configuration SVA/SVR a été mise à jour et prend immédiatement effet pour les nouveaux dossiers."
redirect_to admin_procedure_path(@procedure)
else
flash.now.alert = "Des erreurs empêchent la validation du SVA/SVR. Corrigez les erreurs"
render :edit
end
end
private
def configuration_params
params.require(:sva_svr_configuration).permit(:decision, :period, :unit, :resume)
end
end
end

View file

@ -234,7 +234,7 @@ module Instructeurs
commentaire = CommentaireService.build(current_instructeur, dossier, { body: message, piece_jointe: })
if commentaire.valid?
dossier.flag_as_pending_correction!(commentaire)
dossier.flag_as_pending_correction!(commentaire, params[:kind].presence)
dossier.update!(last_commentaire_updated_at: Time.zone.now)
current_instructeur.follow(dossier)

View file

@ -185,6 +185,7 @@ module Users
if errors.blank?
@dossier.passer_en_construction!
@dossier.process_declarative!
@dossier.process_sva_svr!
NotificationMailer.send_en_construction_notification(@dossier).deliver_later
@dossier.groupe_instructeur.instructeurs.with_instant_email_dossier_notifications.each do |instructeur|
DossierMailer.notify_new_dossier_depose_to_instructeur(@dossier, instructeur.email).deliver_later
@ -233,6 +234,7 @@ module Users
if cast_bool(params.dig(:dossier, :pending_correction_confirm))
editing_fork_origin.resolve_pending_correction!
editing_fork_origin.process_sva_svr!
end
redirect_to dossier_path(editing_fork_origin)

View file

@ -8,7 +8,7 @@ module ProcedureHelper
def procedure_badge(procedure)
return nil unless procedure.brouillon?
tag.span(t('helpers.procedure.testing_procedure'), class: 'fr-badge')
tag.span(t('helpers.procedure.testing_procedure'), class: 'fr-badge fr-badge--sm')
end
def procedure_publish_label(procedure, key)

View file

@ -0,0 +1,11 @@
class Cron::ProcedureProcessSVASVRJob < Cron::CronJob
self.schedule_expression = "every day at 1:00"
def perform
Procedure.sva_svr.find_each do |procedure|
procedure.dossiers.state_en_construction_ou_instruction.find_each do |dossier|
ProcedureSVASVRProcessDossierJob.perform_later(dossier)
end
end
end
end

View file

@ -0,0 +1,7 @@
class ProcedureSVASVRProcessDossierJob < ApplicationJob
queue_as :sva
def perform(dossier)
dossier.process_sva_svr!
end
end

View file

@ -53,6 +53,8 @@ class DossierMailer < ApplicationMailer
@dossier = dossier
@service = dossier.procedure.service
@logo_url = attach_logo(dossier.procedure)
@correction = commentaire.dossier_correction
@subject = default_i18n_subject(dossier_id: dossier.id, libelle_demarche: dossier.procedure.libelle)
mail(to: dossier.user_email_for(:notification), subject: @subject) do |format|

View file

@ -8,20 +8,24 @@ module DossierCorrectableConcern
scope :with_pending_corrections, -> { joins(:corrections).where(corrections: { resolved_at: nil }) }
def flag_as_pending_correction!(commentaire)
def flag_as_pending_correction!(commentaire, kind = nil)
return unless may_flag_as_pending_correction?
corrections.create!(commentaire:)
kind ||= :correction
corrections.create!(commentaire:, kind:)
log_pending_correction_operation(commentaire, kind) if procedure.sva_svr_enabled?
return if en_construction?
repasser_en_construction!(instructeur: commentaire.instructeur)
repasser_en_construction_with_pending_correction!(instructeur: commentaire.instructeur)
end
def may_flag_as_pending_correction?
return false if pending_corrections.exists?
en_construction? || may_repasser_en_construction?
en_construction? || may_repasser_en_construction_with_pending_correction?
end
def pending_correction?
@ -39,6 +43,20 @@ module DossierCorrectableConcern
def resolve_pending_correction!
pending_corrections.update!(resolved_at: Time.current)
pending_corrections.reset
end
private
def log_pending_correction_operation(commentaire, kind)
operation = case kind.to_sym
when :correction
"demander_une_correction"
when :incomplete
"demander_a_completer"
end
log_dossier_operation(commentaire.instructeur, operation, commentaire)
end
end
end

View file

@ -12,8 +12,10 @@ module DossierFilteringConcern
scope :filter_by_datetimes, lambda { |column, dates|
if dates.present?
case column
when 'sva_svr_decision_before'
state_not_termine.where("dossiers.sva_svr_decision_on": ..dates.sort.first)
when *DATE_SINCE_MAPPING.keys
where("dossiers.#{DATE_SINCE_MAPPING.fetch(column)} >= ?", dates.sort.first)
where("dossiers.#{DATE_SINCE_MAPPING.fetch(column)}": dates.sort.first..)
else
dates
.map { |date| self.where(column => date..(date + 1.day)) }

View file

@ -0,0 +1,54 @@
module ProcedureSVASVRConcern
extend ActiveSupport::Concern
included do
scope :sva_svr, -> { where("sva_svr ->> 'decision' IN (?)", ['sva', 'svr']) }
validate :sva_svr_immutable_on_published, if: :will_save_change_to_sva_svr?
validate :validates_sva_svr_compatible
def sva_svr_enabled?
sva? || svr?
end
def sva?
decision == :sva
end
def svr?
decision == :svr
end
def sva_svr_configuration
@sva_svr_configuration ||= SVASVRConfiguration.new(sva_svr)
end
def sva_svr_decision
decision
end
private
def decision
sva_svr.fetch("decision", nil)&.to_sym
end
def decision_was
sva_svr_was.fetch("decision", nil)&.to_sym
end
def sva_svr_immutable_on_published
return if brouillon?
return if [:sva, :svr].exclude?(decision_was)
errors.add(:sva_svr, :immutable)
end
def validates_sva_svr_compatible
return if !sva_svr_enabled?
if declarative_with_state.present?
errors.add(:sva_svr, :declarative_incompatible)
end
end
end
end

View file

@ -36,6 +36,8 @@
# processed_at :datetime
# search_terms :string
# state :string
# sva_svr_decision_on :date
# sva_svr_decision_triggered_at :datetime
# termine_close_to_expiration_notice_sent_at :datetime
# created_at :datetime
# updated_at :datetime
@ -199,6 +201,10 @@ class Dossier < ApplicationRecord
end
event :repasser_en_construction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction, guard: :can_repasser_en_construction?
end
event :repasser_en_construction_with_pending_correction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction
end
@ -208,6 +214,7 @@ class Dossier < ApplicationRecord
event :accepter_automatiquement, after: :after_accepter_automatiquement do
transitions from: :en_construction, to: :accepte, guard: :can_accepter_automatiquement?
transitions from: :en_instruction, to: :accepte, guard: :can_accepter_automatiquement?
end
event :refuser, after: :after_refuser do
@ -563,11 +570,23 @@ class Dossier < ApplicationRecord
end
def can_accepter_automatiquement?
declarative_triggered_at.nil? && procedure.declarative_accepte? && can_terminer?
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?)
false
end
def can_passer_automatiquement_en_instruction?
(declarative_triggered_at.nil? && procedure.declarative_en_instruction?) || procedure.auto_archive_on&.then { _1 <= Time.zone.today }
return true if declarative_triggered_at.nil? && procedure.declarative_en_instruction?
return true if procedure.auto_archive_on? && !procedure.auto_archive_on.future?
return true if procedure.sva_svr_enabled? && sva_svr_decision_triggered_at.nil? && !pending_correction?
false
end
def can_repasser_en_construction?
!procedure.sva_svr_enabled?
end
def can_repasser_en_instruction?
@ -900,13 +919,22 @@ class Dossier < ApplicationRecord
def after_passer_automatiquement_en_instruction
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.declarative_triggered_at = self.traitements
.passer_en_instruction
.processed_at
self.en_instruction_at = traitements.passer_en_instruction.processed_at
if procedure.declarative_en_instruction?
self.declarative_triggered_at = en_instruction_at
end
save!
NotificationMailer.send_en_instruction_notification(self).deliver_later
log_automatic_dossier_operation(:passer_en_instruction)
if procedure.sva_svr_enabled?
# TODO: handle serialization errors when SIRET demandeur was not completed
log_automatic_dossier_operation(:passer_en_instruction, self)
else
log_automatic_dossier_operation(:passer_en_instruction)
end
end
def after_repasser_en_construction(h)
@ -975,9 +1003,15 @@ class Dossier < ApplicationRecord
end
def after_accepter_automatiquement
self.processed_at = self.en_instruction_at = self.declarative_triggered_at = self.traitements
.accepter_automatiquement
.processed_at
self.processed_at = traitements.accepter_automatiquement.processed_at
if procedure.declarative_accepte?
self.en_instruction_at = self.processed_at
self.declarative_triggered_at = self.processed_at
elsif procedure.sva_svr_enabled?
self.sva_svr_decision_triggered_at = self.processed_at
end
save!
if attestation.nil?
@ -1046,6 +1080,26 @@ class Dossier < ApplicationRecord
end
end
def process_sva_svr!
return unless procedure.sva_svr_enabled?
return if sva_svr_decision_triggered_at.present?
# set or recompute sva date, except for dossiers submitted before sva was enabled
if depose_at.today? || sva_svr_decision_on.present?
self.sva_svr_decision_on = SVASVRDecisionDateCalculatorService.new(self, procedure).decision_date
end
return if sva_svr_decision_on.nil?
if en_construction? && may_passer_automatiquement_en_instruction?
passer_automatiquement_en_instruction!
elsif en_instruction? && procedure.sva? && may_accepter_automatiquement?
accepter_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
end
def remove_titres_identite!
champs_public.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
end
@ -1134,10 +1188,11 @@ 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,
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
]
].compact
if procedure.routing_enabled?
columns << ['Groupe instructeur', groupe_instructeur.label]
@ -1233,6 +1288,10 @@ class Dossier < ApplicationRecord
false
end
def sva_svr_decision_in_days
(sva_svr_decision_on - Date.current).to_i
end
private
def create_missing_traitemets

View file

@ -3,6 +3,7 @@
# Table name: dossier_corrections
#
# id :bigint not null, primary key
# kind :string default("correction"), not null
# resolved_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
@ -17,6 +18,8 @@ class DossierCorrection < ApplicationRecord
scope :pending, -> { where(resolved_at: nil) }
enum kind: { correction: 'correction', incomplete: 'incomplete' }
def resolved?
resolved_at.present?
end

View file

@ -19,6 +19,8 @@ class DossierOperationLog < ApplicationRecord
changer_groupe_instructeur: 'changer_groupe_instructeur',
passer_en_instruction: 'passer_en_instruction',
repasser_en_construction: 'repasser_en_construction',
demander_une_correction: 'demander_une_correction',
demander_a_completer: 'demander_a_completer',
repasser_en_instruction: 'repasser_en_instruction',
accepter: 'accepter',
refuser: 'refuser',
@ -134,6 +136,8 @@ class DossierOperationLog < ApplicationRecord
SerializerService.champ(subject)
when Avis
SerializerService.avis(subject)
when Commentaire
SerializerService.message(subject)
end
end
end

View file

@ -47,6 +47,7 @@
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# routing_enabled :boolean
# sva_svr :jsonb not null
# tags :text default([]), is an Array
# unpublished_at :datetime
# web_hook_url :string
@ -68,6 +69,7 @@ class Procedure < ApplicationRecord
include EncryptableConcern
include InitiationProcedureConcern
include ProcedureGroupeInstructeurAPIHackConcern
include ProcedureSVASVRConcern
include Discard::Model
self.discard_column = :hidden_at

View file

@ -38,6 +38,8 @@ 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),
@ -45,13 +47,15 @@ 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),
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),
field_hash('self', 'en_instruction_since', type: :date, virtual: true),
field_hash('self', 'processed_since', type: :date, virtual: true),
field_hash('self', 'state', type: :enum, scope: 'instructeurs.dossiers.filterable_state', virtual: true)
]
].compact_blank
end
def fields
@ -118,11 +122,15 @@ 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')
]
array << field_hash('self', 'sva_svr_decision_on', classname: 'sva-col') if procedure.sva_svr_enabled?
array
end
def sorted_ids(dossiers, count)

View file

@ -0,0 +1,34 @@
class SVASVRConfiguration
include ActiveModel::Model
include ActiveModel::Attributes
attribute :decision, default: 'disabled'
attribute :period, default: 2
attribute :unit, default: 'months'
attribute :resume, default: 'continue'
DECISION_OPTIONS = ['disabled', 'sva', 'svr']
UNIT_OPTIONS = ['days', 'weeks', 'months']
RESUME_OPTIONS = ['continue', 'reset']
validates :decision, inclusion: { in: DECISION_OPTIONS.without('svr') }
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? }
def self.unit_options
UNIT_OPTIONS
end
def human_decision
return if decision == 'disabled'
decision.upcase
end
private
def enabled?
decision != 'disabled'
end
end

View file

@ -1,5 +1,5 @@
class DossierProjectionService
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :batch_operation_id, :corrections, :columns) do
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :batch_operation_id, :sva_svr_decision_on, :corrections, :columns) do
def pending_correction?
return false if corrections.blank?
@ -29,8 +29,9 @@ class DossierProjectionService
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' }
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }
([state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field, dossier_corrections] + fields) # the view needs state and archived dossier attributes
([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field, dossier_corrections] + fields) # the view needs state and archived dossier attributes
.each { |f| f[:id_value_h] = {} }
.group_by { |f| f[TABLE] } # one query per table
.each do |table, fields|
@ -54,7 +55,7 @@ class DossierProjectionService
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
.each do |id, *columns|
fields.zip(columns).each do |field, value|
if [state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field].include?(field)
if [state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field, sva_svr_decision_on_field].include?(field)
field[:id_value_h][id] = value
else
field[:id_value_h][id] = value&.strftime('%d/%m/%Y') # other fields are datetime
@ -130,6 +131,7 @@ class DossierProjectionService
hidden_by_user_at_field[:id_value_h][dossier_id],
hidden_by_administration_at_field[:id_value_h][dossier_id],
batch_operation_field[:id_value_h][dossier_id],
sva_svr_decision_on_field[:id_value_h][dossier_id],
dossier_corrections[:id_value_h][dossier_id],
fields.map { |f| f[:id_value_h][dossier_id] }
)

View file

@ -41,6 +41,15 @@ class SerializerService
end
end
def self.message(commentaire)
Sentry.with_scope do |scope|
scope.set_tags(dossier_id: commentaire.dossier_id)
data = execute_query('serializeMessage', { number: commentaire.dossier_id, id: commentaire.to_typed_id })
data && data['dossier']["messages"].first
end
end
def self.execute_query(operation_name, variables)
result = API::V2::Schema.execute(QUERY,
variables: variables,
@ -113,6 +122,14 @@ class SerializerService
}
}
query serializeMessage($number: Int!, $id: ID!) {
dossier(number: $number) {
messages(id: $id) {
...MessageFragment
}
}
}
fragment DossierFragment on Dossier {
id
number
@ -359,5 +376,15 @@ class SerializerService
}
}
}
fragment MessageFragment on Message {
id
email
body
createdAt
attachments {
...FileFragment
}
}
GRAPHQL
end

View file

@ -0,0 +1,77 @@
class SVASVRDecisionDateCalculatorService
attr_reader :dossier, :procedure, :unit, :period, :resume_method
EMPTY_DOSSIER = Struct.new(:depose_at) do
def corrections
[]
end
end
def self.decision_date_from_today(procedure)
dossier = EMPTY_DOSSIER.new(Date.current)
new(dossier, procedure).decision_date
end
def initialize(dossier, procedure)
@dossier = dossier
@procedure = procedure
config = procedure.sva_svr_configuration
@unit = config.unit.to_sym
@period = config.period.to_i
@resume_method = config.resume.to_sym
end
def decision_date
duration = calculate_duration
start_date = determine_start_date + 1.day
correction_delay = calculate_correction_delay(start_date)
start_date + correction_delay + duration
end
private
def calculate_duration
case unit
when :days
period.days
when :weeks
period.weeks
when :months
period.months
end
end
def determine_start_date
return dossier.depose_at.to_date if dossier.corrections.empty?
return latest_correction_date if resume_method == :reset
return latest_incomplete_correction_date if dossier.corrections.any?(&:incomplete?)
dossier.depose_at.to_date
end
def latest_incomplete_correction_date
correction_date dossier.corrections.filter(&:incomplete?).max_by(&:resolved_at)
end
def latest_correction_date
correction_date dossier.corrections.max_by(&:resolved_at)
end
def calculate_correction_delay(start_date)
dossier.corrections.sum do |correction|
resolved_date = correction_date(correction)
next 0 unless resolved_date > start_date
(resolved_date + 1.day - correction.created_at.to_date).days # restart from next day after resolution
end
end
def correction_date(correction)
# NOTE: when correction is not resolved, assume it could be done today
# so interfaces could show how many days are remaining after correction
correction.resolved_at&.to_date || Date.current
end
end

View file

@ -63,5 +63,6 @@
= render Procedure::Card::AnnotationsComponent.new(procedure: @procedure)
= render Procedure::Card::APIEntrepriseComponent.new(procedure: @procedure)
= render Procedure::Card::APIParticulierComponent.new(procedure: @procedure)
= render Procedure::Card::SVASVRComponent.new(procedure: @procedure) if @procedure.sva_svr_enabled? || @procedure.feature_enabled?(:sva)
= render Procedure::Card::MonAvisComponent.new(procedure: @procedure)
= render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure)

View file

@ -0,0 +1,43 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
["#{@procedure.libelle.truncate_words(10)}", admin_procedure_path(@procedure)],
["Configuration SVA/SVR"]] }
.fr-container.fr-my-5w
%h1.fr-h1 Règle du Silence Vaut Accord ou Silence Vaut Rejet
= render Dsfr::CalloutComponent.new(title: "Fonctionnement du SVA/SVR") do |c|
- c.with_body do
%p.fr-callout__text
Le SVA “Silence Vaut Accord” ou SVR “Silence Vaut Rejet” est un principe législatif qui définit le comportement dune demande en cas de silence de l'administration : soit la demande est automatiquement acceptée (SVA), soit elle est automatiquement rejetée (SVR).
%strong= APPLICATION_NAME
permet lapplication de ce principe pour les démarches concernées.
%p.fr-callout__text.fr-mt-2w
Concrètement, le silence est labsence dinstruction dun dossier par un instructeur à lissue du délai imparti. À son dépôt, un dossier passe immédiatement “en instruction” (lusager ne peut plus le modifier) et le calcul du délai démarrera le lendemain. À lissue du délai, si le dossier na pas été instruit par un instructeur, il sera automatiquement accepté ou refusé, comme si un instructeur lavait fait manuellement. Toutes les autres fonctionnalités restent inchangées.
%p.fr-callout__text.fr-mt-2w
Lécoulement du délai est suspendu quand un instructeur
%strong demande des corrections
ou
déclare le
%strong dossier incomplet
(actions
%em Demander une correction
et
%em Demander à compléter
). Le dossier repasse alors en construction.
Une fois les corrections effectuées, lusager re-dépose son dossier. En fonction de la démarche et de la complétude du dossier, lécoulement du délai reprend, ou est réinitialisé à 0.
%p.fr-callout__text.fr-mt-2w
Cet écran permet le réglage des paramètres nécessaires au fonctionnement du SVA ou SVR.
%br
%strong Il sera ensuite impossible de modifier ou désactiver cette fonctionnalité sur une démarche publiée.
- c.with_bottom do
%p.fr-mt-2w
= link_to("Texte sur LegiFrance", "https://www.legifrance.gouv.fr/contenu/menu/autour-de-la-loi/sva-silence-vaut-accord", class: "fr-link fr-mr-1w", title: new_tab_suffix("En savoir plus sur le LegiFrance"), **external_link_attributes)
= link_to("Liste des démarches encadrées par ce principe", "https://www.service-public.fr/demarches-silence-vaut-accord", class: "fr-link", title: new_tab_suffix("Rechercher les démarches avec SVA sur service-public.fr"), **external_link_attributes)
= render Procedure::SVASVRFormComponent.new(procedure: @procedure, configuration: @configuration)

View file

@ -3,10 +3,13 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
%p= t('.explanation_html', dossier_id: @dossier.id, libelle_demarche: @dossier.procedure.libelle)
%p= t(".#{@correction.kind}.explanation_html", dossier_id: @dossier.id, libelle_demarche: @dossier.procedure.libelle)
%p= t('.link')
= round_button(t('.access_message'), messagerie_dossier_url(@dossier), :primary)
- if @dossier.sva_svr_decision_on.present?
%p= t(".#{@correction.kind}.sva_svr", rule_name: t(@dossier.procedure.sva? ? :sva : :svr, scope: 'shared.procedures.sva_svr_rule_name'))
= render 'layouts/mailers/signature', service: @service
- content_for :footer do

View file

@ -241,6 +241,18 @@ def add_etats_dossier(pdf, dossier)
format_in_2_columns(pdf, "En instruction le", try_format_date(dossier.en_instruction_at))
end
if dossier.sva_svr_decision_triggered_at.present?
format_in_2_columns(pdf, "Décision #{dossier.procedure.sva_svr_configuration.human_decision} prise le", try_format_date(dossier.sva_svr_decision_triggered_at))
elsif dossier.sva_svr_decision_on.present?
value = if dossier.pending_correction?
"#{dossier.sva_svr_decision_in_days} jours après la correction"
else
try_format_date(dossier.sva_svr_decision_on)
end
format_in_2_columns(pdf, "Date prévisionnelle #{dossier.procedure.sva_svr_configuration.human_decision}", value)
end
if dossier.processed_at.present?
format_in_2_columns(pdf, "Décision le", try_format_date(dossier.processed_at))
end

View file

@ -4,11 +4,15 @@
%h1.fr-h3.fr-mb-1w
= "Dossier nº #{dossier.id}"
= status_badge(dossier.state, 'super')
= pending_correction_badge(:for_instructeur) if dossier.pending_correction?
= link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link"
= procedure_badge(dossier.procedure)
.fr-mt-2w.badge-group
= procedure_badge(dossier.procedure)
= status_badge(dossier.state)
= pending_correction_badge(:for_instructeur) if dossier.pending_correction?
= render Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: dossier, procedure: dossier.procedure, with_label: true)
.header-actions.fr-ml-auto
= render partial: 'instructeurs/dossiers/header_actions', locals: { dossier: }

View file

@ -46,7 +46,7 @@
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to passer_en_instruction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-icon-edit-line' do
Passer en instruction
- elsif Dossier.states[:en_instruction] == state && !with_menu
- elsif Dossier.states[:en_instruction] == state && !with_menu && !sva_svr
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to repasser_en_construction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-btn--secondary fr-icon-draft-line' do
Repasser en construction

View file

@ -174,6 +174,11 @@
- status << pending_correction_badge(:for_instructeur, html_class: "fr-mt-1v") if p.pending_correction?
= link_to_if(p.hidden_by_administration_at.blank?, safe_join(status), path, class: class_names("cell-link": true, "fr-py-0": status.many?))
- if @procedure.sva_svr_enabled?
%td
%span.cell-link
= link_to_if p.hidden_by_administration_at.blank?, render(Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: p, procedure: @procedure), path)
%td.action-col.follow-col
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
= render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure_id: @procedure.id,
@ -183,6 +188,7 @@
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: @statut == 'expirant',
hidden_by_administration: @statut == 'supprimes_recemment',
sva_svr: @procedure.sva_svr_enabled?,
turbo: false,
with_menu: false }
%tfoot

View file

@ -101,6 +101,7 @@
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: nil,
hidden_by_administration: nil,
sva_svr: p.sva_svr_decision_on.present?,
turbo: false,
with_menu: false }

View file

@ -23,7 +23,7 @@
= yield
- unless @no_description
.fr-accordions-group
.fr-accordions-group.fr-mb-3w
%section.fr-accordion
%h2.fr-accordion__title
%button.fr-accordion__btn{ "aria-controls" => "accordion-114", "aria-expanded" => "true" }
@ -77,5 +77,16 @@
#accordion-117.fr-collapse
= t('shared.procedure_description.estimated_fill_duration_detail', estimated_minutes: estimated_fill_duration_minutes(procedure))
.fr-my-3w
= render Procedure::NoticeComponent.new(procedure:)
.fr-my-3w
= render Procedure::NoticeComponent.new(procedure:)
- if procedure.sva_svr_enabled?
= render Dsfr::CalloutComponent.new(title: t('shared.procedure_description.sva_svr_title', rule: t(procedure.sva_svr_decision, scope: 'shared.procedures.sva_svr_rule_name')), icon: "fr-fi-information-line", extra_class_names: "fr-my-6w") do |c|
- c.with_body do
%p
= t("#{procedure.sva_svr_decision}_text_html", scope: 'shared.procedure_description') # i18n-tasks-use: t('shared.procedure_description.sva_text_html') t('shared.procedure_description.svr_text_html')
%p.fr-mt-1w
= t('shared.procedure_description.sva_svr_prevision_date',
delay: t("x_#{procedure.sva_svr_configuration.unit}", count: procedure.sva_svr_configuration.period.to_i, scope: 'datetime.distance_in_words'),
date: l(SVASVRDecisionDateCalculatorService.decision_date_from_today(procedure), format: :long).gsub(' ', " "))

View file

@ -43,6 +43,13 @@
%p{ role: 'status' }
= t('views.users.dossiers.show.status_overview.admin_review')
- if dossier.sva_svr_decision_on.present?
-# i18n-tasks-use t('views.users.dossiers.show.status_overview.delay_title.sva'), t('views.users.dossiers.show.status_overview.delay_title.svr')
= render Dsfr::CalloutComponent.new(title: t(dossier.procedure.sva_svr_configuration.decision, scope: "views.users.dossiers.show.status_overview.delay_title")) do |c|
- c.with_body do
%p
= t('views.users.dossiers.show.status_overview.delay_text_sva_svr', date: l(dossier.sva_svr_decision_on, format: :long))
= render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure }
%p

View file

@ -19,7 +19,8 @@ features = [
:procedure_routage_api,
:groupe_instructeur_api_hack,
:rerouting,
:cojo_type_de_champ
:cojo_type_de_champ,
:sva
]
def database_exists?

View file

@ -15,6 +15,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'JSON'
inflect.acronym 'RNA'
inflect.acronym 'URL'
inflect.acronym 'SVA'
inflect.acronym 'SVR'
inflect.irregular 'type_de_champ', 'types_de_champ'
inflect.irregular 'type_de_champ_private', 'types_de_champ_private'
inflect.irregular 'procedure_revision_type_de_champ', 'procedure_revision_types_de_champ'

View file

@ -432,6 +432,10 @@ en:
en_construction_html: Your file is in progress. It means that <strong>you can still edit it</strong>. You will no longer be able to edit the file when the administration will switch it to "review".
status_review: undergoing review
admin_review: The administration is reviewing your file. You are no longer able to edit it.
delay_title:
sva: "Your file is subject to the legislative framework « Silence Vaut Accord »"
svr: "Your file is subject to the legislative framework « Silence Vaut Rejet »"
delay_text_sva_svr: "You will receive a reply from the administration no later than %{date}."
status_completed: completed
use_mailbox_for_questions_html: "<strong>You have a question?</strong> Use the mailbox to <a href=\"%{mailbox_url}\">contact the administration directly</a>."
acceptee_html: "Your file had been <strong>accepted</strong>."
@ -833,9 +837,19 @@ en:
dossiers_count: "Nb files"
weekly_distribution: "Weekly distribution"
weekly_distribution_details: "in the last 6 months"
sva_svr_rule_name:
sva: "Silence Vaut Accord"
svr: "Silence Vaut Rejet"
procedure_description:
estimated_fill_duration: "Estimated fill time: %{estimated_minutes} mn"
estimated_fill_duration_title: What is the procedure estimated fill time ?
estimated_fill_duration_detail: "The fill time is etimated to %{estimated_minutes} min. This period may vary depending on the options you choose"
pieces_jointes : What are the required attachments ?
pieces_jointes_conditionnal_list_title : Attachments list according to your situation
sva_svr_title: "This procedure applies the « %{rule} »"
sva_text_html: "SVA, or « <strong>Silence Vaut Rejet</strong> » is a legislative principle that defines the behavior of a request in the event of silence from the administration: the request is automatically accepted after a certain period if your file is complete."
svr_text_html: "SVR, or « <strong>Silence Vaut Rejet</strong> » is a legislative principle that defines the behavior of a request in the event of silence from the administration: the request is automatically refused after a certain period if your file is complete."
sva_svr_prevision_date:
Thus, you will receive an answer to your request within %{delay} of submitting your complete file.
For example, if you submit your application today, you will receive a reply no later than %{date}.
If your file is incomplete, this date may be postponed until submitting a completed file.

View file

@ -434,6 +434,10 @@ fr:
en_construction_html: "Votre dossier est en construction. Cela signifie que <strong>vous pouvez encore le modifier</strong>. Vous ne pourrez plus modifier votre dossier lorsque ladministration le passera « en instruction »."
status_review: en instruction
admin_review: Votre dossier est en cours dinstruction par ladministration. Vous ne pouvez plus le modifier.
delay_title:
sva: "Votre dossier est soumis au cadre législatif « Silence Vaut Accord »"
svr: "Votre dossier est soumis au cadre législatif « Silence Vaut Rejet »"
delay_text_sva_svr: "Vous aurez un retour de l'administration au plus tard le %{date}."
status_completed: terminé
use_mailbox_for_questions_html: "<strong>Vous avez une question ?</strong> Utilisez la messagerie pour <a href=\"%{mailbox_url}\">contacter ladministration directement</a>."
acceptee_html: "Votre dossier a été <strong>accepté</strong>."
@ -887,9 +891,19 @@ fr:
dossiers_count: "Nb dossiers"
weekly_distribution: "Répartition par semaine"
weekly_distribution_details: "au cours des 6 derniers mois"
sva_svr_rule_name:
sva: "Silence Vaut Accord"
svr: "Silence Vaut Rejet"
procedure_description:
estimated_fill_duration: "Temps de remplissage estimé : %{estimated_minutes} mn"
estimated_fill_duration_title: Quelle est la durée de remplissage de la démarche ?
estimated_fill_duration_detail: "La durée de remplissage est estimée à %{estimated_minutes} min. Ce délai peut varier selon les options que vous choisirez."
pieces_jointes : Quelles sont les pièces justificatives à fournir ?
pieces_jointes_conditionnal_list_title : Liste des pièces en fonction de votre situation
sva_svr_title: "Cette démarche applique le « %{rule} »"
sva_text_html: "Le SVA, ou « <strong>Silence Vaut Accord</strong> », est un principe législatif qui définit le comportement dune demande en cas dabsence de réponse de ladministration : la demande sera automatiquement acceptée à lissue dun délai si votre dossier est complet."
svr_text_html: "Le SVR, ou « <strong>Silence Vaut Rejet</strong> », est un principe législatif qui définit le comportement dune demande en cas dabsence de réponse de ladministration : la demande sera automatiquement refusée à lissue dun délai si votre dossier est complet."
sva_svr_prevision_date:
Ainsi, vous recevrez une réponse à votre demande dans les %{delay} après avoir déposé votre dossier complet.
Par exemple, si vous déposez votre dossier aujourd'hui, vous aurez une réponse au plus tard le %{date}.
Si votre dossier est incomplet, cette date pourrait être repoussée jusquà la soumission dun dossier complété.

View file

@ -57,3 +57,6 @@ en:
format: 'Private field %{message}'
lien_dpo:
invalid_uri_or_email: "Fill in with an email or a link"
sva_svr:
immutable: "SVA/SVR configuration can no longer be modified"
declarative_incompatible: "SVA/SVR is incompatible with a declarative procedure"

View file

@ -65,3 +65,6 @@ fr:
invalid_uri_or_email: "Veuillez saisir un mail ou un lien"
auto_archive_on:
future: doit être dans le futur
sva_svr:
immutable: "La configuration SVA/SVR ne peut plus être modifiée"
declarative_incompatible: "Le SVA/SVR est incompatible avec une démarche déclarative"

View file

@ -17,6 +17,8 @@ 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
user:
email: Requester
followers_instructeurs:

View file

@ -17,6 +17,8 @@ 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
user:
email: Demandeur
followers_instructeurs:

View file

@ -6,6 +6,15 @@
:message_date_with_year => lambda { |time, _| "%B #{time.day.ordinalize} %Y at %H:%M" },
:message_date_without_time => lambda { |_time, _| "%Y/%m/%d" }
}
},
datetime: {
distance_in_words: {
x_weeks: {
one: "1 week",
other: "%{count} weeks"
}
}
}
}
}

View file

@ -13,6 +13,15 @@
:message_date_with_year => lambda { |time, _| "le #{time.day == 1 ? '1er' : time.day} %B %Y à %H h %M" },
:message_date_without_time => lambda { |_time, _| "%d/%m/%Y" }
}
},
datetime: {
distance_in_words: {
x_weeks: {
one: "1 semaine",
other: "%{count} semaines"
}
}
}
}
}

View file

@ -3,8 +3,16 @@ en:
dossier_mailer:
notify_pending_correction:
subject: You need to modify your file no. %{dossier_id} « %{libelle_demarche} »
explanation_html:
In order to continue its instruction, <strong>an instructor requested you to edit information</strong> to your file no. %{dossier_id} of the « %{libelle_demarche} » procedure.
correction:
explanation_html:
In order to continue its instruction, <strong>an instructor requested you to edit information</strong> to your file no. %{dossier_id} of the « %{libelle_demarche} » procedure.
sva_svr:
As part of the « %{rule_name} » principle, the instruction period is suspended until you submit the requested corrections.
incomplete:
explanation_html:
In order to continue its instruction, <strong>an instructor requested you to complete</strong> your file no. %{dossier_id} of the « %{libelle_demarche} » procedure.
sva_svr:
As part of the « %{rule_name} » principle, the instruction period will be reset upon receipt of your complete file.
link:
Check your file's mailbox to see what changes need to be made, then edit the file directly on the website.
access_message: Open the mailbox

View file

@ -3,8 +3,16 @@ fr:
dossier_mailer:
notify_pending_correction:
subject: Vous devez corriger votre dossier nº %{dossier_id} « %{libelle_demarche} »
explanation_html:
Afin de poursuivre son instruction, <strong>un instructeur vous demande dapporter des corrections</strong> à votre dossier nº %{dossier_id} de la démarche « %{libelle_demarche} ».
correction:
explanation_html:
Afin de poursuivre son instruction, <strong>un instructeur vous demande dapporter des corrections</strong> à votre dossier nº %{dossier_id} de la démarche « %{libelle_demarche} ».
sva_svr:
Dans le cadre du principe du « %{rule_name} », le délai dinstruction est suspendu jusquà ce que vous déposiez les corrections demandées.
incomplete:
explanation_html:
Afin de commencer son instruction, <strong>un instructeur vous demande de compléter</strong> votre dossier nº %{dossier_id} de la démarche « %{libelle_demarche} ».
sva_svr:
Dans le cadre du principe du « %{rule_name} », le délai dinstruction sera réinitialisé à réception de votre dossier complet.
link:
Consultez la messagerie de votre dossier pour prendre connaissance des modifications à effectuer,
puis modifiez le dossier directement sur le site.

View file

@ -573,6 +573,8 @@ Rails.application.routes.draw do
resource :dossier_submitted_message, only: [:edit, :update, :create]
# ADDED TO ACCESS IT FROM THE IFRAME
get 'attestation_template/preview' => 'attestation_templates#preview'
resource :sva_svr, only: [:show, :edit, :update], controller: 'sva_svr'
end
resources :services, except: [:show] do

View file

@ -0,0 +1,5 @@
class AddSVASVRToProcedures < ActiveRecord::Migration[7.0]
def change
add_column :procedures, :sva_svr, :jsonb, default: {}, null: false
end
end

View file

@ -0,0 +1,6 @@
class AddSVASVRDecisionOnToDossiers < ActiveRecord::Migration[7.0]
def change
add_column :dossiers, :sva_svr_decision_on, :date, default: nil
add_column :dossiers, :sva_svr_decision_triggered_at, :datetime, default: nil
end
end

View file

@ -0,0 +1,5 @@
class AddKindToDossierCorrections < ActiveRecord::Migration[7.0]
def change
add_column :dossier_corrections, :kind, :string, default: 'correction', null: false
end
end

View file

@ -323,6 +323,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_29_102031) do
t.bigint "dossier_id", null: false
t.datetime "resolved_at", precision: 6
t.datetime "updated_at", precision: 6, null: false
t.string "kind", default: "correction", null: false
t.index ["commentaire_id"], name: "index_dossier_corrections_on_commentaire_id"
t.index ["dossier_id"], name: "index_dossier_corrections_on_dossier_id"
t.index ["resolved_at"], name: "index_dossier_corrections_on_resolved_at", where: "((resolved_at IS NULL) OR (resolved_at IS NOT NULL))"
@ -409,6 +410,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_29_102031) do
t.string "state"
t.datetime "termine_close_to_expiration_notice_sent_at", precision: 6
t.datetime "updated_at", precision: 6
t.date "sva_svr_decision_on"
t.datetime "sva_svr_decision_triggered_at", precision: 6
t.integer "user_id"
t.index ["archived"], name: "index_dossiers_on_archived"
t.index ["batch_operation_id"], name: "index_dossiers_on_batch_operation_id"
@ -762,6 +765,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_29_102031) do
t.text "routing_criteria_name", default: "Votre ville"
t.boolean "routing_enabled"
t.bigint "service_id"
t.jsonb "sva_svr", default: {}, null: false
t.text "tags", default: [], array: true
t.datetime "test_started_at", precision: 6
t.datetime "unpublished_at", precision: 6

View file

@ -49,5 +49,16 @@ RSpec.describe Instructeurs::EnConstructionMenuComponent, type: :component do
expect(subject).to have_dropdown_item('Repasser en construction')
expect(subject).to have_dropdown_items(count: 3)
end
context 'when procedure is sva' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :sva)) }
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)
end
end
end
end

View file

@ -499,11 +499,13 @@ describe Instructeurs::DossiersController, type: :controller do
describe '#pending_correction' do
let(:message) { 'do that' }
let(:justificatif) { nil }
let(:kind) { nil }
subject do
post :pending_correction, params: {
procedure_id: procedure.id, dossier_id: dossier.id,
dossier: { motivation: message, justificatif_motivation: justificatif }
dossier: { motivation: message, justificatif_motivation: justificatif },
kind:
}, format: :turbo_stream
end
@ -529,6 +531,7 @@ describe Instructeurs::DossiersController, type: :controller do
expect(dossier.reload).to be_en_construction
expect(dossier).to be_pending_correction
expect(dossier.corrections.last).to be_correction
end
it 'create a comment with text body' do
@ -536,6 +539,14 @@ describe Instructeurs::DossiersController, type: :controller do
expect(dossier.commentaires.last).to be_flagged_pending_correction
end
context 'flagged as incomplete' do
let(:kind) { 'incomplete' }
it 'create a correction of incomplete kind' do
expect(dossier.corrections.last).to be_incomplete
end
end
context 'with an attachment' do
let(:justificatif) { fake_justificatif }

View file

@ -1,4 +1,6 @@
describe Users::DossiersController, type: :controller do
include ActiveSupport::Testing::TimeHelpers
let(:user) { create(:user) }
describe 'before_actions' do
@ -351,9 +353,8 @@ describe Users::DossiersController, type: :controller do
let(:payload) { { id: dossier.id } }
subject do
Timecop.freeze(now) do
post :submit_brouillon, params: payload
end
travel_to now
post :submit_brouillon, params: payload
end
context 'when the dossier cannot be updated by the user' do
@ -433,6 +434,24 @@ describe Users::DossiersController, type: :controller do
it { expect(flash.alert).to eq("Vous navez pas accès à ce dossier") }
end
end
context 'when procedure has sva enabled' do
let(:procedure) { create(:procedure, :sva) }
let!(:dossier) { create(:dossier, :brouillon, procedure:, user:) }
it 'passe automatiquement en instruction' do
delivery = double.tap { expect(_1).to receive(:deliver_later).with(no_args).twice }
expect(NotificationMailer).to receive(:send_en_construction_notification).and_return(delivery)
expect(NotificationMailer).to receive(:send_en_instruction_notification).and_return(delivery)
subject
dossier.reload
expect(dossier).to be_en_instruction
expect(dossier.pending_correction?).to be_falsey
expect(dossier.en_instruction_at).to within(5.seconds).of(Time.current)
end
end
end
describe '#submit_en_construction' do
@ -522,6 +541,47 @@ describe Users::DossiersController, type: :controller do
it "resolve correction" do
expect { subject }.to change { correction.reload.resolved_at }.to be_truthy
end
context 'when procedure has sva enabled' do
let(:procedure) { create(:procedure, :sva) }
let!(:dossier) { create(:dossier, :en_construction, procedure:, user:) }
it 'passe automatiquement en instruction' do
expect(dossier.pending_correction?).to be_truthy
subject
dossier.reload
expect(dossier).to be_en_instruction
expect(dossier.pending_correction?).to be_falsey
expect(dossier.en_instruction_at).to within(5.seconds).of(Time.current)
end
end
end
context 'when there is sva without confirming correction' do
let!(:correction) { create(:dossier_correction, dossier: dossier) }
subject { post :submit_en_construction, params: { id: dossier.id } }
it "does not resolve correction" do
expect { subject }.not_to change { correction.reload.resolved_at }
end
context 'when procedure has sva enabled' do
let(:procedure) { create(:procedure, :sva) }
let!(:dossier) { create(:dossier, :en_construction, procedure:, user:) }
it 'does not passe automatiquement en instruction' do
expect(dossier.pending_correction?).to be_truthy
subject
dossier.reload
expect(dossier).to be_en_construction
expect(dossier.pending_correction?).to be_truthy
end
end
end
end

View file

@ -1,6 +1,7 @@
FactoryBot.define do
factory :commentaire do
association :dossier, :en_construction
email { generate(:user_email) }
body { 'plop' }

View file

@ -2,6 +2,7 @@ FactoryBot.define do
factory :dossier_correction do
dossier
commentaire
kind { :correction }
resolved_at { nil }
trait :resolved do

View file

@ -13,6 +13,8 @@ FactoryBot.define do
ask_birthday { false }
lien_site_web { "https://mon-site.gouv" }
path { SecureRandom.uuid }
declarative_with_state { nil }
sva_svr { {} }
groupe_instructeurs { [association(:groupe_instructeur, :default, procedure: instance, strategy: :build)] }
administrateurs { administrateur.present? ? [administrateur] : [association(:administrateur)] }
@ -411,6 +413,14 @@ FactoryBot.define do
build(:dossier_submitted_message, revisions: [procedure.active_revision])
end
end
trait :sva do
sva_svr { SVASVRConfiguration.new(decision: :sva).attributes }
end
trait :svr do
sva_svr { SVASVRConfiguration.new(decision: :svr).attributes }
end
end
end

View file

@ -0,0 +1,33 @@
RSpec.describe Cron::ProcedureProcessSVASVRJob, type: :job do
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, depose_at: 2.months.ago, sva_svr_decision_on: Date.current) }
let!(:dossier_in_future) { create(:dossier, :en_instruction, :with_individual, procedure:, depose_at: 1.day.ago, sva_svr_decision_on: Date.yesterday + 2.months) }
let!(:dossier_en_construction) { create(:dossier, :en_construction, :with_individual, procedure:, depose_at: 2.months.ago, sva_svr_decision_on: Date.current) }
let!(:dossier_en_brouillon) { create(:dossier, :brouillon, :with_individual, procedure:) }
subject(:perform_job) { described_class.perform_now }
before { perform_job }
context 'when procedure is published' do
let(:procedure) { create(:procedure, :published, :sva, :for_individual) }
it 'queues ProcedureSVASVRProcessDossierJob for published sva procedure' do
expect(ProcedureSVASVRProcessDossierJob).to have_been_enqueued.with(dossier)
expect(ProcedureSVASVRProcessDossierJob).to have_been_enqueued.with(dossier_en_construction)
expect(ProcedureSVASVRProcessDossierJob).to have_been_enqueued.with(dossier_in_future)
expect(ProcedureSVASVRProcessDossierJob).not_to have_been_enqueued.with(dossier_en_brouillon)
expect(ProcedureSVASVRProcessDossierJob).to have_been_enqueued.exactly(3).times
end
end
context 'when procedure is closed' do
let(:procedure) { create(:procedure, :closed, :sva, :for_individual) }
it { expect(ProcedureSVASVRProcessDossierJob).to have_been_enqueued.with(dossier) }
end
context 'when procedure is not sva' do
let(:procedure) { create(:procedure, :published, :for_individual) }
it { expect(ProcedureSVASVRProcessDossierJob).not_to have_been_enqueued }
end
end

View file

@ -0,0 +1,56 @@
RSpec.describe ProcedureSVASVRProcessDossierJob, type: :job do
include ActiveJob::TestHelper
include ActiveSupport::Testing::TimeHelpers
let(:procedure) { create(:procedure, :published, :sva, :for_individual) }
let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, depose_at: 2.months.ago - 1.day, sva_svr_decision_on: Date.current) }
subject do
described_class.perform_now(dossier)
dossier.reload
end
context 'when procedure is SVA' do
it 'should accept dossier' do
expect(subject.sva_svr_decision_on).to eq(Date.current)
expect(subject).to be_accepte
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 accept 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 accept 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) }
it 'should be noop' do
expect(subject.sva_svr_decision_on).to be_nil
expect(subject).to be_en_instruction
expect(subject.processed_at).to be_nil
end
end
end

View file

@ -228,4 +228,53 @@ RSpec.describe DossierMailer, type: :mailer do
it { expect(subject.body).to include(dossier.procedure.libelle) }
it { expect(subject.body).to include("Suite à cette modification, vous ne suivez plus ce dossier.") }
end
describe '.notify_pending_correction' do
let(:procedure) { create(:procedure) }
let(:dossier) { create(:dossier, :en_construction, procedure:, sva_svr_decision_on:) }
let(:sva_svr_decision_on) { nil }
let(:kind) { :correction }
let(:commentaire) { create(:commentaire, dossier:) }
subject {
dossier.flag_as_pending_correction!(commentaire, kind)
described_class.with(commentaire:).notify_pending_correction
}
context 'kind is correction' do
it { expect(subject.subject).to eq("Vous devez corriger votre dossier nº #{dossier.id} « #{dossier.procedure.libelle} »") }
it { expect(subject.body).to include("apporter des corrections") }
it { expect(subject.body).not_to include("Silence") }
end
context 'sva with kind is correction' do
let(:sva_svr_decision_on) { Date.tomorrow }
let(:procedure) { create(:procedure, :sva) }
it { expect(subject.subject).to eq("Vous devez corriger votre dossier nº #{dossier.id} « #{dossier.procedure.libelle} »") }
it { expect(subject.body).to include("apporter des corrections") }
it { expect(subject.body).to include("Silence Vaut Accord") }
it { expect(subject.body).to include("suspendu") }
end
context 'sva with kind is incomplete' do
let(:sva_svr_decision_on) { Date.tomorrow }
let(:kind) { :incomplete }
let(:procedure) { create(:procedure, :sva) }
it { expect(subject.body).to include("compléter") }
it { expect(subject.body).to include("Silence Vaut Accord") }
it { expect(subject.body).to include("réinitialisé") }
end
context 'svr with kind is incomplete' do
let(:sva_svr_decision_on) { Date.tomorrow }
let(:kind) { :incomplete }
let(:procedure) { create(:procedure, :svr) }
it { expect(subject.body).to include("compléter") }
it { expect(subject.body).to include("Silence Vaut Rejet") }
it { expect(subject.body).to include("réinitialisé") }
end
end
end

View file

@ -9,7 +9,13 @@ class DossierMailerPreview < ActionMailer::Preview
end
def notify_pending_correction
DossierMailer.with(dossier: dossier_en_construction).notify_pending_correction
commentaire = commentaire(on: dossier_en_construction(sva_svr_decision: :sva)).tap { _1.build_dossier_correction(kind: :correction) }
DossierMailer.with(commentaire:).notify_pending_correction
end
def notify_pending_correction_sva_correction
commentaire = commentaire(on: dossier_en_construction(sva_svr_decision: :sva)).tap { _1.build_dossier_correction(kind: :correction) }
DossierMailer.with(commentaire:).notify_pending_correction
end
def notify_revert_to_instruction
@ -99,8 +105,17 @@ class DossierMailerPreview < ActionMailer::Preview
Dossier.new(id: 47882, state: :en_instruction, procedure: procedure, user: user)
end
def dossier_en_construction
Dossier.new(id: 47882, state: :en_construction, procedure: procedure, user: user)
def dossier_en_construction(sva_svr_decision: nil)
local_procedure = procedure
dossier = Dossier.new(id: 47882, state: :en_construction, procedure: local_procedure, user: user)
if sva_svr_decision
local_procedure.sva_svr = { decision: sva_svr_decision, period: 2, unit: :months }
dossier.sva_svr_decision_on = 10.days.from_now.to_date
end
dossier
end
def dossier_accepte

View file

@ -31,25 +31,33 @@ describe DossierCorrectableConcern do
let(:instructeur) { create(:instructeur) }
let(:commentaire) { create(:commentaire, dossier:, instructeur:) }
subject(:flag) { dossier.flag_as_pending_correction!(commentaire) }
context 'when dossier is en_construction' do
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
expect { flag }.to change { dossier.corrections.pending.count }.by(1)
expect(dossier.corrections.last).to be_correction
end
it 'created a correction of incomplete kind' do
expect { dossier.flag_as_pending_correction!(commentaire, "incomplete") }.to change { dossier.corrections.pending.count }.by(1)
expect(dossier.corrections.last).to be_incomplete
end
it 'does not change dossier state' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.state }
expect { flag }.not_to change { dossier.state }
end
end
context 'when dossier is not en_instruction' do
context 'when dossier is en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
expect { flag }.to change { dossier.corrections.pending.count }.by(1)
end
it 'repasse dossier en_construction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.state }.to('en_construction')
expect { flag }.to change { dossier.state }.to('en_construction')
end
end
@ -57,7 +65,7 @@ describe DossierCorrectableConcern do
before { create(:dossier_correction, dossier:) }
it 'does not create a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.corrections.pending.count }
expect { flag }.not_to change { dossier.corrections.pending.count }
end
end
@ -65,7 +73,7 @@ describe DossierCorrectableConcern do
before { create(:dossier_correction, :resolved, dossier:) }
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
expect { flag }.to change { dossier.corrections.pending.count }.by(1)
end
end
@ -73,7 +81,39 @@ describe DossierCorrectableConcern do
let(:dossier) { create(:dossier, :accepte) }
it 'does not create a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.corrections.pending.count }
expect { flag }.not_to change { dossier.corrections.pending.count }
end
end
context 'when procedure is sva' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :published, :sva)) }
it 'creates a correction' do
expect { flag }.to change { dossier.corrections.pending.count }.by(1)
end
it 'repasse dossier en_construction' do
expect { flag }.to change { dossier.state }.to('en_construction')
end
it 'creates a log operation' do
expect { flag }.to change { dossier.dossier_operation_logs.count }.by(2)
log_correction, log_construction = dossier.dossier_operation_logs.last(2)
expect(log_correction.operation).to eq("demander_une_correction")
expect(log_construction.operation).to eq("repasser_en_construction")
expect(log_correction.data["subject"]["body"]).to eq(commentaire.body)
expect(log_correction.data["subject"]["email"]).to eq(commentaire.instructeur.email)
end
it 'creates a log operation of incomplete dossier' do
expect { dossier.flag_as_pending_correction!(commentaire, "incomplete") }.to change { dossier.dossier_operation_logs.count }.by(2)
log_correction, _ = dossier.dossier_operation_logs.last(2)
expect(log_correction.operation).to eq("demander_a_completer")
expect(log_correction.data["subject"]["body"]).to eq(commentaire.body)
expect(log_correction.data["subject"]["email"]).to eq(commentaire.instructeur.email)
end
end
end

View file

@ -1011,7 +1011,6 @@ describe Dossier, type: :model do
end
describe '#accepter_automatiquement!' do
let(:dossier) { create(:dossier, :en_construction, :with_individual, :with_declarative_accepte) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let!(:now) { Time.zone.parse('01/01/2100') }
let(:attestation) { Attestation.new }
@ -1021,20 +1020,49 @@ describe Dossier, type: :model do
allow(dossier).to receive(:build_attestation).and_return(attestation)
Timecop.freeze(now)
dossier.accepter_automatiquement!
dossier.reload
end
after { Timecop.return }
it { expect(dossier.motivation).to eq(nil) }
it { expect(dossier.en_instruction_at).to eq(now) }
it { expect(dossier.processed_at).to eq(now) }
it { expect(dossier.state).to eq('accepte') }
it { expect(last_operation.operation).to eq('accepter') }
it { expect(last_operation.automatic_operation?).to be_truthy }
it { expect(NotificationMailer).to have_received(:send_accepte_notification).with(dossier) }
it { expect(dossier.attestation).to eq(attestation) }
subject {
dossier.accepter_automatiquement!
dossier.reload
}
context 'as declarative procedure' do
let(:dossier) { create(:dossier, :en_construction, :with_individual, :with_declarative_accepte) }
it 'accepts dossier automatiquement' do
expect(subject.motivation).to eq(nil)
expect(subject.en_instruction_at).to eq(now)
expect(subject.processed_at).to eq(now)
expect(subject.declarative_triggered_at).to eq(now)
expect(subject.sva_svr_decision_triggered_at).to be_nil
expect(subject).to be_accepte
expect(last_operation.operation).to eq('accepter')
expect(last_operation.automatic_operation?).to be_truthy
expect(NotificationMailer).to have_received(:send_accepte_notification).with(dossier)
expect(subject.attestation).to eq(attestation)
end
end
context 'as sva procedure' do
let(:procedure) { create(:procedure, :for_individual, :published, :sva) }
let(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:, sva_svr_decision_on: Date.current, en_instruction_at: DateTime.new(2021, 5, 1, 12)) }
it 'accepts dossier automatiquement' do
expect(subject.motivation).to eq(nil)
expect(subject.en_instruction_at).to eq(DateTime.new(2021, 5, 1, 12))
expect(subject.processed_at).to eq(now)
expect(subject.declarative_triggered_at).to be_nil
expect(subject.sva_svr_decision_triggered_at).to eq(now)
expect(subject).to be_accepte
expect(last_operation.operation).to eq('accepter')
expect(last_operation.automatic_operation?).to be_truthy
expect(NotificationMailer).to have_received(:send_accepte_notification).with(dossier)
expect(subject.attestation).to eq(attestation)
end
end
end
describe '#passer_en_instruction!' do
@ -1062,20 +1090,70 @@ describe Dossier, type: :model do
end
describe '#passer_automatiquement_en_instruction!' do
let(:dossier) { create(:dossier, :en_construction, :with_declarative_en_instruction, en_construction_close_to_expiration_notice_sent_at: Time.zone.now) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { last_operation.data }
let(:instructeur) { create(:instructeur) }
before { dossier.passer_automatiquement_en_instruction! }
context "via procedure declarative en instruction" do
let(:dossier) { create(:dossier, :en_construction, :with_declarative_en_instruction, en_construction_close_to_expiration_notice_sent_at: Time.zone.now) }
it { expect(dossier.followers_instructeurs).not_to include(instructeur) }
it { expect(dossier.en_construction_close_to_expiration_notice_sent_at).to be_nil }
it { expect(last_operation.operation).to eq('passer_en_instruction') }
it { expect(last_operation.automatic_operation?).to be_truthy }
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
it { expect(operation_serialized['dossier_id']).to eq(dossier.id) }
it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) }
subject do
dossier.process_declarative!
dossier.reload
end
it 'passes dossier en instruction' do
expect(subject.followers_instructeurs).not_to include(instructeur)
expect(subject.en_construction_close_to_expiration_notice_sent_at).to be_nil
expect(subject.declarative_triggered_at).to be_within(1.second).of(Time.current)
expect(last_operation.operation).to eq('passer_en_instruction')
expect(last_operation.automatic_operation?).to be_truthy
expect(operation_serialized['operation']).to eq('passer_en_instruction')
expect(operation_serialized['dossier_id']).to eq(dossier.id)
expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601)
end
end
context "via procedure sva" do
let(:procedure) { create(:procedure, :sva, :published, :for_individual) }
let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:, sva_svr_decision_on: 10.days.from_now) }
subject do
dossier.process_sva_svr!
dossier.reload
end
it 'passes dossier en instruction' do
expect(subject.state).to eq('en_instruction')
expect(subject.followers_instructeurs).not_to include(instructeur)
expect(subject.sva_svr_decision_on).to eq(2.months.from_now.to_date + 1.day) # date is updated
expect(last_operation.operation).to eq('passer_en_instruction')
expect(last_operation.automatic_operation?).to be_truthy
expect(operation_serialized['operation']).to eq('passer_en_instruction')
expect(operation_serialized['dossier_id']).to eq(dossier.id)
expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601)
end
context 'when dossier was submitted with sva not yet enabled' do
let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:, depose_at: 10.days.ago) }
it 'leaves dossier en construction' do
expect(subject.sva_svr_decision_on).to be_nil
expect(subject.state).to eq('en_construction')
end
end
end
end
describe '#can_repasser_en_construction?' do
let(:dossier) { create(:dossier, :en_instruction) }
it { expect(dossier.can_repasser_en_construction?).to be_truthy }
context 'when procedure is sva' do
let(:dossier) { create(:dossier, :en_instruction, procedure: create(:procedure, :sva)) }
it { expect(dossier.can_repasser_en_construction?).to be_falsey }
end
end
describe '#can_passer_automatiquement_en_instruction?' do
@ -1115,10 +1193,24 @@ describe Dossier, type: :model do
it { expect(dossier.can_passer_automatiquement_en_instruction?).to be_truthy }
end
end
context 'when procedure has sva or svr enabled' do
let(:procedure) { create(:procedure, :published, :sva) }
let(:dossier) { create(:dossier, :en_construction, procedure:) }
it { expect(dossier.can_passer_automatiquement_en_instruction?).to be_truthy }
context 'when dossier was already processed by sva' do
let(:dossier) { create(:dossier, :en_construction, procedure:, sva_svr_decision_triggered_at: 1.hour.ago) }
it { expect(dossier.can_passer_automatiquement_en_instruction?).to be_falsey }
end
end
end
describe '#can_accepter_automatiquement?' do
let(:dossier) { create(:dossier, :en_instruction, declarative_triggered_at: declarative_triggered_at) }
let(:dossier) { create(:dossier, state: initial_state, declarative_triggered_at: declarative_triggered_at) }
let(:initial_state) { :en_construction }
let(:declarative_triggered_at) { nil }
it { expect(dossier.can_accepter_automatiquement?).to be_falsey }
@ -1136,6 +1228,43 @@ describe Dossier, type: :model do
it { expect(dossier.can_accepter_automatiquement?).to be_falsey }
end
end
context 'when procedure is sva/svr' do
let(:decision) { :sva }
let(:initial_state) { :en_instruction }
before do
dossier.procedure.update!(sva_svr: SVASVRConfiguration.new(decision:).attributes)
dossier.update!(sva_svr_decision_on: Date.current)
end
it { expect(dossier.can_accepter_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_accepter_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_accepter_automatiquement?).to be_falsey }
end
context 'when decision is svr' do
let(:decision) { :svr }
it { expect(dossier.can_accepter_automatiquement?).to be_falsey }
end
context 'when dossier was already processed by sva' do
before { dossier.update!(sva_svr_decision_triggered_at: 1.hour.ago) }
it { expect(dossier.can_accepter_automatiquement?).to be_falsey }
end
end
end
describe "can't transition to terminer when etablissement is in degraded mode" do
@ -1752,6 +1881,12 @@ describe Dossier, type: :model do
let(:dossier) { create(:dossier) }
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) }
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]) }
end
end
describe '#processed_in_month' do
@ -1918,6 +2053,12 @@ describe Dossier, type: :model do
end
end
describe '#sva_svr_decision_in_days' do
let(:dossier) { create(:dossier, :en_instruction, sva_svr_decision_on: 10.days.from_now) }
it { expect(dossier.sva_svr_decision_in_days).to eq 10 }
end
private
def count_for_month(processed_by_month, month)

View file

@ -1,4 +1,6 @@
describe ProcedurePresentation do
include ActiveSupport::Testing::TimeHelpers
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}], types_de_champ_private: [{}]) }
let(:instructeur) { create(:instructeur) }
let(:assign_to) { create(:assign_to, procedure: procedure, instructeur: instructeur) }
@ -113,6 +115,18 @@ describe ProcedurePresentation do
it { is_expected.to include(name_field, surname_field, gender_field) }
end
context 'when the procedure is sva/svr' do
let(:procedure) { create(:procedure, :for_individual, :sva) }
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
let(:decision_on) { { "label" => "Date décision SVA", "table" => "self", "column" => "sva_svr_decision_on", 'classname' => '', 'virtual' => false, "type" => :date, "scope" => '', "value_column" => :value } }
let(:decision_before_field) { { "label" => "Date décision SVA avant", "table" => "self", "column" => "sva_svr_decision_before", 'classname' => '', 'virtual' => true, "type" => :date, "scope" => '', "value_column" => :value } }
subject { procedure_presentation.fields }
it { is_expected.to include(decision_on, decision_before_field) }
end
end
describe "#displayable_fields_for_select" do
@ -466,6 +480,23 @@ describe ProcedurePresentation do
it { is_expected.to match_array([kept_dossier.id, later_dossier.id]) }
end
context 'for sva_svr_decision_before column' do
before do
travel_to Time.zone.local(2023, 6, 10, 10)
end
let(:procedure) { create(:procedure, :published, :sva, types_de_champ_public: [{}], types_de_champ_private: [{}]) }
let(:filter) { [{ 'table' => 'self', 'column' => 'sva_svr_decision_before', 'value' => '15/06/2023' }] }
let!(:kept_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current) }
let!(:later_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
let!(:discarded_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 10.days) }
let!(:en_construction_dossier) { create(:dossier, :en_construction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
let!(:accepte_dossier) { create(:dossier, :accepte, procedure:, sva_svr_decision_on: Date.current + 2.days) }
it { is_expected.to match_array([kept_dossier.id, later_dossier.id, en_construction_dossier.id]) }
end
context 'ignore time of day' do
let(:filter) { [{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018 19:30' }] }

View file

@ -401,6 +401,55 @@ describe Procedure do
end
end
end
context 'with sva svr' do
before {
procedure.sva_svr["decision"] = "svr"
}
context 'when procedure is published with sva' do
let(:procedure) { create(:procedure, :published, :sva) }
it 'prevents changes to sva_svr' do
expect(procedure).not_to be_valid
expect(procedure.errors[:sva_svr].join).to include('ne peut plus être modifiée')
end
end
context 'when procedure is published without sva' do
let(:procedure) { create(:procedure, :published) }
it 'allow activation' do
expect(procedure).to be_valid
end
it 'allow activation from disabled value' do
procedure.sva_svr["decision"] = "disabled"
procedure.save!
procedure.sva_svr["decision"] = "svr"
expect(procedure).to be_valid
end
end
context 'brouillon procedure' do
let(:procedure) { create(:procedure, :sva) }
it "can update sva config" do
expect(procedure).to be_valid
end
end
context "with declarative" do
let(:procedure) { create(:procedure, declarative_with_state: "accepte") }
it 'is not valid' do
expect(procedure).not_to be_valid
expect(procedure.errors[:sva_svr].join).to include('incompatible avec une démarche déclarative')
end
end
end
end
describe 'opendata' do

View file

@ -0,0 +1,75 @@
require 'rails_helper'
describe SVASVRConfiguration, type: :model do
subject(:sva_svr_config) do
SVASVRConfiguration.new(
decision: decision,
period: period,
unit: unit,
resume: resume
)
end
let(:decision) { 'disabled' }
let(:period) { 2 }
let(:unit) { 'months' }
let(:resume) { 'continue' }
describe 'validations' do
context 'when decision is "disabled"' do
it 'is valid even if period, unit and resume are nil' do
sva_svr_config.period = nil
sva_svr_config.unit = nil
sva_svr_config.resume = nil
expect(sva_svr_config).to be_valid
end
end
context 'when decision is not in DECISION_OPTIONS' do
let(:decision) { 'invalid_decision' }
it 'is not valid' do
expect(sva_svr_config).not_to be_valid
end
end
context 'when decision is not "disabled"' do
let(:decision) { 'sva' }
it { expect(sva_svr_config).to be_valid }
it 'is not valid if period is nil' do
sva_svr_config.period = nil
expect(sva_svr_config).not_to be_valid
end
it 'is not valid if unit is nil or not in UNIT_OPTIONS' do
sva_svr_config.unit = nil
expect(sva_svr_config).not_to be_valid
sva_svr_config.unit = 'years'
expect(sva_svr_config).not_to be_valid
end
it 'is not valid if resume is nil or not in RESUME_OPTIONS' do
sva_svr_config.resume = nil
expect(sva_svr_config).not_to be_valid
sva_svr_config.resume = 'pause'
expect(sva_svr_config).not_to be_valid
end
it 'is not valid if period is not an integer' do
sva_svr_config.period = 3.14
expect(sva_svr_config).not_to be_valid
end
end
end
end

View file

@ -0,0 +1,167 @@
require 'rails_helper'
describe SVASVRDecisionDateCalculatorService do
include ActiveSupport::Testing::TimeHelpers
let(:procedure) { create(:procedure, sva_svr: config) }
let(:dossier) { create(:dossier, :en_instruction, procedure:, depose_at: DateTime.new(2023, 5, 15, 12)) }
describe '#decision_date' do
subject { described_class.new(dossier, procedure).decision_date }
context 'when sva has a months period' do
let(:config) { { decision: :sva, period: 2, unit: :months, resume: :continue } }
it 'calculates the date based on SVA rules' do
expect(subject).to eq(Date.new(2023, 7, 16))
end
end
context 'when sva has a days period' do
let(:config) { { decision: :sva, period: 30, unit: :days, resume: :continue } }
it 'calculates the date based on SVA rules' do
expect(subject).to eq(Date.new(2023, 6, 15))
end
end
context 'when sva has a weeks period' do
let(:config) { { decision: :sva, period: 8, unit: :weeks, resume: :continue } }
it 'calculates the date based on SVA rules' do
expect(subject).to eq(Date.new(2023, 7, 11))
end
end
context 'when sva resume setting is continue' do
let(:config) { { decision: :sva, period: 2, unit: :months, resume: :continue } }
context 'when a dossier is corrected and resolved' do
let!(:correction) do
created_at = DateTime.new(2023, 5, 20, 15)
resolved_at = DateTime.new(2023, 5, 25, 12)
create(:dossier_correction, dossier:, created_at:, resolved_at:)
end
it 'calculates the date based on SVA rules with correction delay' do
expect(subject).to eq(Date.new(2023, 7, 22))
end
context 'when there are multiple corrections' do
let!(:correction2) do
created_at = DateTime.new(2023, 5, 30, 18)
resolved_at = DateTime.new(2023, 6, 3, 8)
create(:dossier_correction, dossier:, created_at:, resolved_at:)
end
it 'calculates the date based on SVA rules with all correction delays' do
expect(subject).to eq(Date.new(2023, 7, 27))
end
end
context 'there is a pending correction kind = correct' do
before do
travel_to DateTime.new(2023, 5, 30, 18) do
dossier.flag_as_pending_correction!(build(:commentaire, dossier:))
end
travel_to DateTime.new(2023, 6, 5, 8) # 6 days elapsed, restart 1 day after resolved
end
it 'calculates the date, like if resolution will be today' do
expect(subject).to eq(Date.new(2023, 7, 29))
end
end
context 'there is a pending correction kind = incomplete' do
before do
travel_to DateTime.new(2023, 5, 30, 18) do
dossier.flag_as_pending_correction!(build(:commentaire, dossier:), :incomplete)
end
travel_to DateTime.new(2023, 6, 5, 8) # 6 days elapsed
end
it 'calculates the date, like if resolution will be today' do
expect(subject).to eq(Date.new(2023, 8, 6))
end
end
context 'when correction was for an incomplete dossier' do
let!(:correction) do
created_at = DateTime.new(2023, 5, 20, 15)
resolved_at = DateTime.new(2023, 5, 25, 12)
create(:dossier_correction, :incomplete, dossier:, created_at:, resolved_at:)
end
it 'calculates the date by resetting delay' do
expect(subject).to eq(Date.new(2023, 7, 26))
end
context 'when there are multiple corrections' do
let!(:correction2) do
created_at = DateTime.new(2023, 5, 30, 18)
resolved_at = DateTime.new(2023, 6, 3, 8)
create(:dossier_correction, dossier:, created_at:, resolved_at:)
end
it 'calculates the date based on SVA rules with all correction delays' do
expect(subject).to eq(Date.new(2023, 7, 31))
end
end
end
end
end
context 'when sva resume setting is reset' do
let(:config) { { decision: :sva, period: 2, unit: :months, resume: :reset } }
context 'there is no correction' do
it 'calculates the date based on deposed_at' do
expect(subject).to eq(Date.new(2023, 7, 16))
end
end
context 'there are multiple resolved correction' do
before do
created_at = DateTime.new(2023, 5, 16, 15)
resolved_at = DateTime.new(2023, 5, 17, 12)
create(:dossier_correction, :incomplete, dossier:, created_at:, resolved_at:)
created_at = DateTime.new(2023, 5, 20, 15)
resolved_at = DateTime.new(2023, 5, 25, 12)
create(:dossier_correction, dossier:, created_at:, resolved_at:)
end
it 'calculates the date based on SVA rules from the last resolved date' do
expect(subject).to eq(Date.new(2023, 7, 26))
end
end
context 'there is a pending correction' do
before do
travel_to DateTime.new(2023, 5, 30, 18) do
dossier.flag_as_pending_correction!(build(:commentaire, dossier:))
end
travel_to DateTime.new(2023, 6, 5, 8)
end
it 'calculates the date, like if resolution will be today and delay restarted' do
expect(subject).to eq(Date.new(2023, 8, 6))
end
end
end
end
describe '#decision_date_from_today' do
let(:config) { { decision: :sva, period: 2, unit: :months, resume: :continue } }
before { travel_to DateTime.new(2023, 4, 15, 12) }
subject { described_class.decision_date_from_today(procedure) }
it 'calculates the date based on today' do
expect(subject).to eq(Date.new(2023, 6, 16))
end
end
end

View file

@ -21,6 +21,15 @@ describe "procedure filters" do
end
end
scenario "should display sva by default if procedure has sva enabled" do
procedure.update!(sva_svr: SVASVRConfiguration.new(decision: :sva).attributes)
visit instructeur_procedure_path(procedure)
within ".dossiers-table" do
expect(page).to have_link("Date décision SVA")
expect(page).to have_link(new_unfollow_dossier.user.email)
end
end
scenario "should list all dossiers" do
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id.to_s)

View file

@ -31,6 +31,28 @@ describe "procedure sort", js: true do
expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier.id.to_s)
end
scenario "should be able to sort with header with sva date" do
procedure.update!(sva_svr: SVASVRConfiguration.new(decision: :sva).attributes)
followed_dossier_2.update!(sva_svr_decision_on: Date.tomorrow)
followed_dossier.update!(sva_svr_decision_on: Date.today)
visit instructeur_procedure_path(procedure, statut: "suivis")
# sorted by notifications (updated_at desc) by default, filtered by followed
expect(all(".dossiers-table tbody tr").count).to eq(3)
expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s)
expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s)
find("thead .sva-col a").click # sort by sva date asc
expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s)
expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s)
find("thead .sva-col a").click # reverse order - sort by sva date desc
expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier_2.id.to_s)
expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier.id.to_s)
end
scenario "should be able to sort with direct link to notification sort" do
# the real input checkbox is hidden - DSFR set a fake checkbox with a label, so we can't use "check/uncheck" methods
# but we can assert on the hidden checkbox state

View file

@ -11,6 +11,7 @@ describe 'shared/_procedure_description', type: :view do
expect(rendered).to have_text(procedure.description)
expect(rendered).to have_text('Temps de remplissage estimé')
expect(rendered).not_to have_text('Quelles sont les pièces justificatives à fournir')
expect(rendered).not_to have_text('Quest-ce que le cadre législatif « silence vaut accord » ?')
end
context 'procedure with estimated duration not visible' do
@ -70,4 +71,28 @@ describe 'shared/_procedure_description', type: :view do
expect(rendered).to have_text('une description des pj manuelle')
end
end
context 'when the procedure is sva' do
before { travel_to DateTime.new(2023, 1, 1) }
let(:procedure) { create(:procedure, :published, :sva) }
it 'shows an explanation text' do
subject
expect(rendered).to have_text('Cette démarche applique le « Silence Vaut Accord »')
expect(rendered).to have_text('dans les 2 mois')
expect(rendered).to have_text("2 mars 2023")
end
context 'when unit is weeks' do
before {
procedure.sva_svr["unit"] = "weeks"
}
it 'shows an human period' do
subject
expect(rendered).to have_text('dans les 2 semaines')
expect(rendered).to have_text("16 janvier 2023")
end
end
end
end