diff --git a/app/components/dossiers/errors_full_messages_component.rb b/app/components/dossiers/errors_full_messages_component.rb index fd8bafd94a7..1ae164c81cf 100644 --- a/app/components/dossiers/errors_full_messages_component.rb +++ b/app/components/dossiers/errors_full_messages_component.rb @@ -3,13 +3,12 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent ErrorDescriptor = Data.define(:anchor, :label, :error_message) - def initialize(dossier:, errors:) + def initialize(dossier:) @dossier = dossier - @errors = errors end def dedup_and_partitioned_errors - formated_errors = @errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that + formated_errors = @dossier.errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that .to_a # but enum.to_a gives back an array .uniq { |error| [error.inner_error.base] } # dedup cumulated errors from dossier.champs, dossier.champs_public, dossier.champs_private which run the validator one time per association .map { |error| to_error_descriptor(error) } @@ -27,6 +26,6 @@ def to_error_descriptor(error) end def render? - !@errors.empty? + !@dossier.errors.empty? end end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index a8ffc0a8e9c..674d01329b8 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -229,9 +229,9 @@ def brouillon def submit_brouillon @dossier = dossier_with_champs(pj_template: false) - @errors = submit_dossier_and_compute_errors + submit_dossier_and_compute_errors - if @errors.blank? + if @dossier.errors.blank? && @dossier.can_passer_en_construction? @dossier.passer_en_construction! @dossier.process_declarative! @dossier.process_sva_svr! @@ -240,6 +240,7 @@ def submit_brouillon end redirect_to merci_dossier_path(@dossier) else + @dossier.check_in_eligibilite_rules_and_visible_champs if !@dossier.can_passer_en_construction? respond_to do |format| format.html { render :brouillon } format.turbo_stream @@ -276,17 +277,17 @@ def submit_en_construction editing_fork_origin.resolve_pending_correction end - @errors = submit_dossier_and_compute_errors + submit_dossier_and_compute_errors - if @errors.blank? + if @dossier.errors.blank? && @dossier.can_passer_en_construction? editing_fork_origin.merge_fork(@dossier) editing_fork_origin.submit_en_construction! redirect_to dossier_path(editing_fork_origin) else + @dossier.check_in_eligibilite_rules_and_visible_champs if !@dossier.can_passer_en_construction? respond_to do |format| format.html do - @dossier = editing_fork_origin render :modifier end @@ -574,21 +575,14 @@ def update_dossier_and_compute_errors def submit_dossier_and_compute_errors @dossier.validate(:champs_public_value) - - errors = @dossier.errors - @dossier.check_mandatory_and_visible_champs.each do |error_on_champ| - errors.import(error_on_champ) - end + @dossier.check_mandatory_and_visible_champs if @dossier.editing_fork_origin&.pending_correction? @dossier.editing_fork_origin.validate(:champs_public_value) @dossier.editing_fork_origin.errors.where(:pending_correction).each do |error| - errors.import(error) + @dossier.errors.import(error) end - end - - errors end def ensure_ownership! diff --git a/app/models/champ.rb b/app/models/champ.rb index 2cb0bb4ec46..0b28c2ca93a 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -102,6 +102,10 @@ def mandatory_blank? mandatory? && blank? end + def in_eligibilite_rules? + blank? && stable_id.in?(dossier.revision&.eligibilite_rules&.sources || []) + end + def blank? value.blank? end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 86962ec8952..f47e9c75d4c 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -935,13 +935,25 @@ def remove_titres_identite! end def check_mandatory_and_visible_champs - champs_for_revision(scope: :public) + champ_errors = champs_for_revision(scope: :public) .filter { _1.child? ? _1.parent.visible? : true } .filter(&:visible?) .filter(&:mandatory_blank?) .map do |champ| champ.errors.add(:value, :missing) end + champ_errors.each { errors.import(_1) } + end + + def check_in_eligibilite_rules_and_visible_champs + ineligible_errors = champs_for_revision(scope: :public) + .filter { _1.child? ? _1.parent.visible? : true } + .filter(&:visible?) + .filter(&:in_eligibilite_rules?) + .map do |champ| + champ.errors.add(:value, :needed_for_eligibilite) + end + ineligible_errors.each { errors.import(_1) } end def eligibilite_rules_computable? diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 534a73f07dd..65ee42348bc 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -10,7 +10,7 @@ = render NestedForms::FormOwnerComponent.new = form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f| - = render Dossiers::ErrorsFullMessagesComponent.new(dossier: @dossier, errors: @errors || []) + = render Dossiers::ErrorsFullMessagesComponent.new(dossier: dossier) %header.mb-6 .fr-highlight %p.fr-text--sm diff --git a/config/locales/models/champs/champs.en.yml b/config/locales/models/champs/champs.en.yml index ead7030628d..02b79cf9b79 100644 --- a/config/locales/models/champs/champs.en.yml +++ b/config/locales/models/champs/champs.en.yml @@ -7,4 +7,4 @@ fr: value: format: '%{message}' missing: must be filled - + needed_for_eligibilite: must be filled in order to know if your file is elegible diff --git a/config/locales/models/champs/champs.fr.yml b/config/locales/models/champs/champs.fr.yml index 090aabf5549..85849e719a5 100644 --- a/config/locales/models/champs/champs.fr.yml +++ b/config/locales/models/champs/champs.fr.yml @@ -7,4 +7,4 @@ fr: value: format: '%{message}' missing: doit être rempli - + needed_for_eligibilite: doît être rempli pour determiner si votre dossier est éligible diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 89cb459f341..5c010ff8d1d 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -397,7 +397,9 @@ describe '#submit_brouillon' do before { sign_in(user) } - let!(:dossier) { create(:dossier, user: user) } + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let(:types_de_champ_public) { [{ type: :text }] } + let!(:dossier) { create(:dossier, user:, procedure:) } let(:first_champ) { dossier.champs_public.first } let(:anchor_to_first_champ) { controller.helpers.link_to first_champ.libelle, brouillon_dossier_path(anchor: first_champ.labelledby_id), class: 'error-anchor' } let(:value) { 'beautiful value' } @@ -438,9 +440,9 @@ render_views let(:error_message) { 'nop' } before do - expect_any_instance_of(Dossier).to receive(:validate).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( - [double(inner_error: double(base: first_champ), message: 'nop')] + allow_any_instance_of(Dossier).to receive(:validate).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( + [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')] ) subject end @@ -460,15 +462,38 @@ render_views let(:value) { nil } + let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] } + before { subject } + + it { expect(response).to render_template(:brouillon) } + it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } + it { expect(response.body).to have_content("doit être rempli") } + end + + context 'when a eligible champ is missing' do + include Logic + render_views + + let(:value) { nil } + let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :integer_number, libelle: 'l2' }] } + let(:yes_no_tdc) { dossier.champs_public.first } + let(:integer_tdc) { dossier.champs_public.last } + + let(:initial_condition) do + ds_or([ + ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)), + ds_eq(champ_value(integer_tdc.stable_id), constant(1)) + ]) + end before do - first_champ.type_de_champ.update(mandatory: true, libelle: 'l') + procedure.published_revision.update(eligibilite_enabled: true, eligibilite_rules: initial_condition, eligibilite_message: :ko) subject end it { expect(response).to render_template(:brouillon) } it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } - it { expect(response.body).to have_content("doit être rempli") } + it { expect(response.body).to have_content("doît être rempli pour determiner si votre dossier est éligible") } end context 'when dossier has no champ' do @@ -547,8 +572,8 @@ render_views before do - expect_any_instance_of(Dossier).to receive(:validate).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( + allow_any_instance_of(Dossier).to receive(:validate).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( [double(inner_error: double(base: first_champ), message: 'nop')] ) @@ -569,6 +594,32 @@ it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } end + context 'when a eligible champ is missing' do + include Logic + render_views + + let(:value) { nil } + let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :integer_number, libelle: 'l2' }] } + let(:yes_no_tdc) { dossier.champs_public.first } + let(:integer_tdc) { dossier.champs_public.last } + + let(:initial_condition) do + ds_or([ + ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)), + ds_eq(champ_value(integer_tdc.stable_id), constant(1)) + ]) + end + + before do + procedure.published_revision.update(eligibilite_enabled: true, eligibilite_rules: initial_condition, eligibilite_message: :ko) + subject + end + + it { expect(response).to render_template(:modifier) } + it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } + it { expect(response.body).to have_content("doît être rempli pour determiner si votre dossier est éligible") } + end + context 'when dossier has no champ' do let(:submit_payload) { { id: dossier.id } } @@ -810,7 +861,11 @@ let(:must_be_greater_than) { 10 } before do - procedure.published_revision.eligibilite_rules = greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than)) + procedure.published_revision.update( + eligibilite_enabled: true, + eligibilite_message: 'lol', + eligibilite_rules: greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than)) + ) procedure.published_revision.save! end render_views @@ -947,8 +1002,8 @@ context 'classic error' do before do - expect_any_instance_of(Dossier).to receive(:save).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( + allow_any_instance_of(Dossier).to receive(:save).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( [message: 'nop', inner_error: double(base: first_champ)] ) subject