From 0712816ab00933e2c772110e046308a9aad13403 Mon Sep 17 00:00:00 2001 From: Camille Regnault Date: Thu, 6 Aug 2026 18:38:49 +0200 Subject: [PATCH 01/15] refactor: fusionne les 2 controllers de choix du mode en un wizard Wicked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace les 2 controllers par un unique Candidate::GroupingWizardController basé sur Wicked (même pattern que MarketApplicationsController), où Précédent/Continuer naviguent toujours d'un cran réel dans une séquence figée plutôt que de recalculer la destination à chaque clic. Centralise la cascade de résolution "prochaine étape requise" dans MarketApplication#next_required_wizard_step, utilisée par sessions_controller, ApplicationModeGuard et l'API éditeur (api/v1/market_applications_controller) pour éviter les divergences entre ces 3 points d'entrée. --- .../api/v1/market_applications_controller.rb | 13 +- .../candidate/application_modes_controller.rb | 65 ---- .../grouping_legal_types_controller.rb | 44 --- .../candidate/grouping_wizard_controller.rb | 149 ++++++++ .../candidate/sessions_controller.rb | 20 +- .../candidate/application_mode_guard.rb | 9 +- app/models/market_application.rb | 15 + .../application_mode.html.erb} | 4 +- .../grouping_legal_type.html.erb} | 12 +- config/routes.rb | 6 +- .../step_definitions/candidacy_mode_steps.rb | 8 +- .../grouping_legal_type_steps.rb | 2 +- .../create_applications_for_mode_spec.rb | 10 + spec/models/market_application_spec.rb | 57 +++ .../api/v1/market_applications_spec.rb | 19 + .../candidate/application_modes_spec.rb | 257 -------------- .../candidate/company_identifications_spec.rb | 2 +- .../candidate/grouping_legal_types_spec.rb | 85 ----- .../candidate/grouping_wizard_spec.rb | 336 ++++++++++++++++++ .../requests/candidate/lot_selections_spec.rb | 2 +- .../candidate/market_applications_spec.rb | 5 +- spec/requests/candidate/sessions_spec.rb | 2 +- 22 files changed, 633 insertions(+), 489 deletions(-) delete mode 100644 app/controllers/candidate/application_modes_controller.rb delete mode 100644 app/controllers/candidate/grouping_legal_types_controller.rb create mode 100644 app/controllers/candidate/grouping_wizard_controller.rb rename app/views/candidate/{application_modes/show.html.erb => grouping_wizard/application_mode.html.erb} (96%) rename app/views/candidate/{grouping_legal_types/show.html.erb => grouping_wizard/grouping_legal_type.html.erb} (88%) delete mode 100644 spec/requests/candidate/application_modes_spec.rb delete mode 100644 spec/requests/candidate/grouping_legal_types_spec.rb create mode 100644 spec/requests/candidate/grouping_wizard_spec.rb diff --git a/app/controllers/api/v1/market_applications_controller.rb b/app/controllers/api/v1/market_applications_controller.rb index 5f2369ad..4f01e00b 100644 --- a/app/controllers/api/v1/market_applications_controller.rb +++ b/app/controllers/api/v1/market_applications_controller.rb @@ -75,11 +75,8 @@ def success_response(market_application) end def application_url_for(market_application) - if market_application.application_mode_choice_required? - return Rails.application.routes.url_helpers.application_mode_candidate_market_application_url( - market_application.identifier - ) - end + target, step = market_application.next_required_wizard_step + return wizard_step_url(target, step) if target Rails.application.routes.url_helpers.step_candidate_market_application_url( market_application.identifier, @@ -87,6 +84,12 @@ def application_url_for(market_application) ) end + def wizard_step_url(market_application, step) + Rails.application.routes.url_helpers.grouping_wizard_step_candidate_market_application_url( + market_application.identifier, step + ) + end + def validate_application_completed return if @market_application.completed? diff --git a/app/controllers/candidate/application_modes_controller.rb b/app/controllers/candidate/application_modes_controller.rb deleted file mode 100644 index f5322d01..00000000 --- a/app/controllers/candidate/application_modes_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module Candidate - class ApplicationModesController < Candidate::ApplicationController - include Candidate::GroupementFeatureGuard - include Candidate::MarketApplicationGuard - - prepend_before_action :find_market_application - before_action :redirect_if_mode_already_chosen - - def show - @already_mandataire = already_mandataire_elsewhere? - @readonly = @market_application.application_mode.present? - @readonly_continue_path = next_step_path(@market_application) if @readonly - end - - def update - result = Candidate::CreateApplicationsForMode.call( - market_application: @market_application, - application_mode: params[:application_mode] - ) - - return handle_success(result) if result.success? - - @already_mandataire = already_mandataire_elsewhere? - @errors = result.errors - render :show, status: :unprocessable_content - end - - private - - def find_market_application - @market_application = MarketApplication.find_by!(identifier: params[:identifier]) - rescue ActiveRecord::RecordNotFound - render plain: "La candidature recherchée n'a pas été trouvée", status: :not_found - end - - def redirect_if_mode_already_chosen - return if @market_application.application_mode.nil? - return if action_name == 'show' && params[:readonly].present? - - redirect_to next_step_path(@market_application) - end - - def next_step_path(market_application) - if market_application.grouping_legal_type_choice_required? - grouping_legal_type_candidate_market_application_path(market_application.identifier) - else - company_identification_candidate_market_application_path(market_application.identifier) - end - end - - def already_mandataire_elsewhere? - Grouping - .joins(:mandataire_market_application) - .where(public_market: @market_application.public_market, market_applications: { siret: @market_application.siret }) - .where.not(market_applications: { id: @market_application.id }) - .exists? - end - - def handle_success(result) - redirect_to next_step_path(result.market_application) - end - end -end diff --git a/app/controllers/candidate/grouping_legal_types_controller.rb b/app/controllers/candidate/grouping_legal_types_controller.rb deleted file mode 100644 index 03da97a3..00000000 --- a/app/controllers/candidate/grouping_legal_types_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module Candidate - class GroupingLegalTypesController < Candidate::ApplicationController - include Candidate::GroupementFeatureGuard - include Candidate::MarketApplicationGuard - - prepend_before_action :find_market_application - before_action :redirect_unless_mandataire - - def show; end - - def update - result = Candidate::SetGroupingLegalType.call( - market_application: @market_application, - legal_type: params[:legal_type] - ) - - return handle_success if result.success? - - @errors = result.errors - render :show, status: :unprocessable_content - end - - private - - def find_market_application - @market_application = MarketApplication.find_by!(identifier: params[:identifier]) - rescue ActiveRecord::RecordNotFound - render plain: "La candidature recherchée n'a pas été trouvée", status: :not_found - end - - def redirect_unless_mandataire - @grouping = Grouping.joins(:mandataire_market_application).find_by(market_applications: { id: @market_application.id }) - return if @grouping - - redirect_to application_mode_candidate_market_application_path(@market_application.identifier) - end - - def handle_success - redirect_to company_identification_candidate_market_application_path(@market_application.identifier) - end - end -end diff --git a/app/controllers/candidate/grouping_wizard_controller.rb b/app/controllers/candidate/grouping_wizard_controller.rb new file mode 100644 index 00000000..6ac65dd2 --- /dev/null +++ b/app/controllers/candidate/grouping_wizard_controller.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Candidate + class GroupingWizardController < Candidate::ApplicationController + include Wicked::Wizard + include Candidate::GroupementFeatureGuard + include Candidate::MarketApplicationGuard + + rescue_from Wicked::Wizard::InvalidStepError, with: :redirect_to_application_mode + + prepend_before_action :set_steps + prepend_before_action :find_market_application + before_action :redirect_unless_mandataire, unless: -> { params[:id] == 'application_mode' } + + def show + case step + when :application_mode then show_application_mode + when :grouping_legal_type then show_grouping_legal_type + end + + render_wizard unless performed? + end + + def update + case step + when :application_mode then update_application_mode + when :grouping_legal_type then update_grouping_legal_type + end + end + + private + + def set_steps + self.steps = if @market_application&.groupement? + %i[application_mode grouping_legal_type] + else + %i[application_mode] + end + end + + def find_market_application + @market_application = MarketApplication.find_by!(identifier: params[:identifier]) + rescue ActiveRecord::RecordNotFound + render plain: "La candidature recherchée n'a pas été trouvée", status: :not_found + end + + def finish_wizard_path + company_identification_candidate_market_application_path(@market_application.identifier) + end + + def redirect_to_application_mode + redirect_to grouping_wizard_step_candidate_market_application_path(@market_application.identifier, :application_mode) + end + + def redirect_unless_mandataire + return if grouping + + redirect_to_application_mode + end + + def next_wizard_step_path(from_step) + counterpart = @market_application.groupement_counterpart + return counterpart_next_wizard_step_path(counterpart) if counterpart + + next_real_step = next_step(from_step) + return finish_wizard_path if next_real_step == Wicked::FINISH_STEP + + wizard_step_path(@market_application, next_real_step) + end + + def counterpart_next_wizard_step_path(counterpart) + return wizard_step_path(counterpart, :grouping_legal_type) if counterpart.grouping_legal_type_choice_required? + + finish_wizard_path + end + + def wizard_step_path(market_application, step) + grouping_wizard_step_candidate_market_application_path(market_application.identifier, step) + end + + # --- application_mode step --- + + def show_application_mode + return redirect_to(next_wizard_step_path(:application_mode)) if @market_application.application_mode.present? && !params[:readonly] + + @already_mandataire = already_mandataire_elsewhere? + @readonly = @market_application.application_mode.present? + @readonly_continue_path = readonly_continue_path if @readonly + end + + def readonly_continue_path + next_wizard_step_path(:application_mode) + end + + def update_application_mode + result = Candidate::CreateApplicationsForMode.call( + market_application: @market_application, + application_mode: params[:application_mode] + ) + + return handle_application_mode_success(result) if result.success? + + @already_mandataire = already_mandataire_elsewhere? + @errors = result.errors + render_wizard(nil, status: :unprocessable_content) + end + + def handle_application_mode_success(result) + @market_application = result.market_application + set_steps + redirect_to next_wizard_step_path(:application_mode) + end + + def already_mandataire_elsewhere? + Grouping + .joins(:mandataire_market_application) + .where(public_market: @market_application.public_market, market_applications: { siret: @market_application.siret }) + .where.not(market_applications: { id: @market_application.id }) + .exists? + end + + # --- grouping_legal_type step --- + + def show_grouping_legal_type + @grouping = grouping + end + + def update_grouping_legal_type + result = Candidate::SetGroupingLegalType.call( + market_application: @market_application, + legal_type: params[:legal_type] + ) + + if result.success? + redirect_to next_wizard_step_path(:grouping_legal_type) + else + @grouping = grouping + @errors = result.errors + render_wizard(nil, status: :unprocessable_content) + end + end + + def grouping + return @grouping if defined?(@grouping) + + @grouping = Grouping.joins(:mandataire_market_application).find_by(market_applications: { id: @market_application.id }) + end + end +end diff --git a/app/controllers/candidate/sessions_controller.rb b/app/controllers/candidate/sessions_controller.rb index cd84b3b1..499825c3 100644 --- a/app/controllers/candidate/sessions_controller.rb +++ b/app/controllers/candidate/sessions_controller.rb @@ -45,16 +45,22 @@ def sign_in_candidate(user, market_application) end def first_step_path(market_application) - if market_application.completed? - return deadline_passed_candidate_market_application_path(market_application.identifier) unless market_application.public_market.open? + return completed_application_path(market_application) if market_application.completed? - return candidate_sync_status_path(market_application.identifier) - end + target, step = market_application.next_required_wizard_step + return company_identification_candidate_market_application_path(market_application.identifier) unless target - return application_mode_candidate_market_application_path(market_application.identifier) if market_application.application_mode_choice_required? - return grouping_legal_type_candidate_market_application_path(market_application.identifier) if market_application.grouping_legal_type_choice_required? + wizard_step_path(target, step) + end + + def completed_application_path(market_application) + return deadline_passed_candidate_market_application_path(market_application.identifier) unless market_application.public_market.open? + + candidate_sync_status_path(market_application.identifier) + end - company_identification_candidate_market_application_path(market_application.identifier) + def wizard_step_path(market_application, step) + grouping_wizard_step_candidate_market_application_path(market_application.identifier, step) end def handle_magic_link_sent(result) diff --git a/app/controllers/concerns/candidate/application_mode_guard.rb b/app/controllers/concerns/candidate/application_mode_guard.rb index 848431f7..de5d7899 100644 --- a/app/controllers/concerns/candidate/application_mode_guard.rb +++ b/app/controllers/concerns/candidate/application_mode_guard.rb @@ -11,9 +11,14 @@ module ApplicationModeGuard private def redirect_to_application_mode_choice - return unless @market_application&.application_mode_choice_required? + return unless @market_application - redirect_to application_mode_candidate_market_application_path(@market_application.identifier) + target, step = @market_application.next_required_wizard_step + redirect_to_wizard_step(step, target) if target + end + + def redirect_to_wizard_step(step, market_application = @market_application) + redirect_to grouping_wizard_step_candidate_market_application_path(market_application.identifier, step) end end end diff --git a/app/models/market_application.rb b/app/models/market_application.rb index 36ee72ea..17e544a0 100644 --- a/app/models/market_application.rb +++ b/app/models/market_application.rb @@ -57,6 +57,21 @@ def grouping_legal_type_choice_required? Grouping.joins(:mandataire_market_application).exists?(legal_type: nil, market_applications: { id: }) end + def groupement_counterpart + return nil if groupement? + + MarketApplication.where(public_market:, siret:, application_mode: :groupement, user_id:).where.not(id:).first + end + + def next_required_wizard_step + return [self, :application_mode] if application_mode_choice_required? + + target = groupement_counterpart || self + return [target, :grouping_legal_type] if target.grouping_legal_type_choice_required? + + nil + end + def update_api_status(api_name, status:, fields_filled: 0) with_lock do updated_status = (api_fetch_status || {}).dup diff --git a/app/views/candidate/application_modes/show.html.erb b/app/views/candidate/grouping_wizard/application_mode.html.erb similarity index 96% rename from app/views/candidate/application_modes/show.html.erb rename to app/views/candidate/grouping_wizard/application_mode.html.erb index ab949d30..792df0a6 100644 --- a/app/views/candidate/application_modes/show.html.erb +++ b/app/views/candidate/grouping_wizard/application_mode.html.erb @@ -28,9 +28,7 @@ <% end %> - <%= form_with url: application_mode_candidate_market_application_path(@market_application.identifier), - method: :patch, - data: { controller: "enable-on-choice" } do |form| %> + <%= form_with url: wizard_path, method: :patch, data: { controller: "enable-on-choice" } do |form| %>
> <%= t('candidate.application_modes.title') %> diff --git a/app/views/candidate/grouping_legal_types/show.html.erb b/app/views/candidate/grouping_wizard/grouping_legal_type.html.erb similarity index 88% rename from app/views/candidate/grouping_legal_types/show.html.erb rename to app/views/candidate/grouping_wizard/grouping_legal_type.html.erb index 632a4385..5f2527dd 100644 --- a/app/views/candidate/grouping_legal_types/show.html.erb +++ b/app/views/candidate/grouping_wizard/grouping_legal_type.html.erb @@ -2,7 +2,7 @@

<%= t('candidate.grouping_legal_types.step_current') %>

-

<%= t('candidate.grouping_legal_types.title') %>

+

<%= t('candidate.grouping_legal_types.title') %>