From 2fd0d9bc864060f5cb68e8947cc8cd060a3fdb14 Mon Sep 17 00:00:00 2001 From: Caillou <6117264+JeSuisUnCaillou@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:10:26 +0100 Subject: [PATCH] add modalitites block to api impot particulier --- app/decorators/authorization_decorator.rb | 7 ++ app/helpers/authorization_requests_helpers.rb | 6 +- ...e_modalite_impot_particulier_controller.js | 80 +++++++++++++------ .../api_impot_particulier.rb | 2 +- .../api_impot_particulier_sandbox.rb | 2 +- .../authorization_extensions/modalities.rb | 28 +++++++ .../api_impot_particulier_modalities.rb | 22 +++++ .../_form.html.erb | 2 + .../build/modalities.html.erb | 4 + .../shared/_hidden_attributes.html.erb | 5 ++ .../shared/_modalities.html.erb | 43 +++++++++- .../new/api_impot_particulier.html.erb | 32 ++++---- .../shared/blocks/_modalities.html.erb | 13 ++- config/authorization_definitions.yml | 2 + .../api_impot_particulier.yml | 2 + config/locales/activerecord.fr.yml | 6 ++ .../authorization_request_forms.fr.yml | 23 ++++++ config/locales/fr.yml | 17 +--- .../choix_formulaire.feature | 33 +++++++- .../api_impot_particulier/editeur.feature | 3 + .../gestion_des_scopes.feature | 3 + .../api_impot_particulier/sandbox.feature | 3 + .../sandbox_gestion_des_scopes.feature | 3 + ...ots_particuliers_editeur_modalites.feature | 46 +++++++++++ features/step_definitions/web_steps.rb | 8 +- spec/factories/authorization_requests.rb | 8 ++ .../api_impot_particulier_spec.rb | 69 +++++++++++++++- spec/support/configure_javascript_driver.rb | 1 + 28 files changed, 406 insertions(+), 67 deletions(-) create mode 100644 app/decorators/authorization_decorator.rb create mode 100644 app/models/concerns/authorization_extensions/modalities.rb create mode 100644 app/models/concerns/dgfip_extensions/api_impot_particulier_modalities.rb create mode 100644 app/views/authorization_request_forms/build/modalities.html.erb create mode 100644 app/views/authorization_request_forms/shared/_hidden_attributes.html.erb create mode 100644 features/habilitations/dgfip/api_impots_particuliers_editeur_modalites.feature diff --git a/app/decorators/authorization_decorator.rb b/app/decorators/authorization_decorator.rb new file mode 100644 index 000000000..d1561a461 --- /dev/null +++ b/app/decorators/authorization_decorator.rb @@ -0,0 +1,7 @@ +class AuthorizationDecorator < ApplicationDecorator + delegate_all + + def name_for_select + "Habilitation du #{slug} : #{name}" + end +end diff --git a/app/helpers/authorization_requests_helpers.rb b/app/helpers/authorization_requests_helpers.rb index 4013e65af..156479a38 100644 --- a/app/helpers/authorization_requests_helpers.rb +++ b/app/helpers/authorization_requests_helpers.rb @@ -1,4 +1,8 @@ module AuthorizationRequestsHelpers + def hidden_attributes + params.slice(:attributes).permit! + end + def start_authorization_request_form(form, disabled: false) text = t('start_authorization_request_form.cta', authorization_name: form.authorization_definition.name) css_classes = %w[fr-btn fr-icon-save-line fr-btn--icon-left] @@ -6,7 +10,7 @@ def start_authorization_request_form(form, disabled: false) if disabled button_tag(text, class: css_classes, disabled: true, id: dom_id(form, :start_authorization_request)) else - link_to(text, start_authorization_request_forms_path(form_uid: form.id), class: css_classes, id: dom_id(form, :start_authorization_request)) + link_to(text, start_authorization_request_forms_path(form_uid: form.id, params: hidden_attributes), class: css_classes, id: dom_id(form, :start_authorization_request)) end end diff --git a/app/javascript/controllers/choose_modalite_impot_particulier_controller.js b/app/javascript/controllers/choose_modalite_impot_particulier_controller.js index 79fbc1634..066035d99 100644 --- a/app/javascript/controllers/choose_modalite_impot_particulier_controller.js +++ b/app/javascript/controllers/choose_modalite_impot_particulier_controller.js @@ -1,39 +1,61 @@ import { Controller } from '@hotwired/stimulus' export default class extends Controller { - static targets = ['franceConnectSelector', 'franceConnectSelectorContainer', 'nextStage', 'links'] + static targets = ['modality', 'franceConnectSelector', 'franceConnectContainer', 'nextStage', 'links'] - trigger (event) { - const selectedOption = event.target.value + trigger () { + const modalityValue = this.modalityTargets.find(modality => modality.checked).value - if (selectedOption === 'with_france_connect') { - this.show(this.franceConnectSelectorContainerTarget) - - if (!this.hasFranceConnectSelectorTarget || !this.franceConnectSelectorTarget.value) { - this.hide(this.nextStageTarget) - } + if (modalityValue === 'with_france_connect') { + this.show(this.franceConnectContainerTarget) + this.showNextStageIfHabilitationSelected() } else { - this.hide(this.franceConnectSelectorContainerTarget) - if (this.hasFranceConnectSelectorTarget) { - this.franceConnectSelectorTarget.value = '' - } + this.hide(this.franceConnectContainerTarget) + this.emptyFranceConnectSelector() this.showNextStage() } - this.updateLinks({ modalite: selectedOption }) + this.addToLinks({ 'attributes[modalities]': modalityValue }) } - showNextStageIfHabilitationSelected (event) { - const franceConnectAuthorizationId = event.target.value - - if (franceConnectAuthorizationId) { - this.updateLinks({ france_connect_authorization_id: franceConnectAuthorizationId }) + showNextStageIfHabilitationSelected () { + if (this.hasFranceConnectSelectorTarget) { + this.franceConnectSelectorTarget.required = 'required' + const franceConnectAuthorizationId = this.valueOrDefaultFranceConnectAuthorizationId() + this.addToLinks({ 'attributes[france_connect_authorization_id]': franceConnectAuthorizationId }) this.showNextStage() + } else { + this.hideNextStage() + } + } + + valueOrDefaultFranceConnectAuthorizationId () { + if (!this.franceConnectSelectorTarget.value) { + const defaultValue = Array.from(this.franceConnectSelectorTarget.options).filter(option => option.value)[0].value + this.franceConnectSelectorTarget.value = defaultValue + } + + return this.franceConnectSelectorTarget.value + } + + emptyFranceConnectSelector () { + if (this.hasFranceConnectSelectorTarget) { + this.franceConnectSelectorTarget.value = null + this.franceConnectSelectorTarget.required = '' + this.removeFromLinks('attributes[france_connect_authorization_id]') } } showNextStage () { - this.show(this.nextStageTarget) + if (this.hasNextStageTarget) { + this.show(this.nextStageTarget) + } + } + + hideNextStage () { + if (this.hasNextStageTarget) { + this.hide(this.nextStageTarget) + } } show (element) { @@ -44,13 +66,13 @@ export default class extends Controller { element.classList.add('fr-hidden') } - updateLinks (params) { + addToLinks (params) { this.linksTargets.forEach((link) => { - this.updateLink(link, params) + this.addToLink(link, params) }) } - updateLink (link, params) { + addToLink (link, params) { const url = new URL(link.href) Object.entries(params).forEach(entry => { @@ -59,4 +81,16 @@ export default class extends Controller { link.href = url } + + removeFromLinks (param) { + this.linksTargets.forEach((link) => { + this.removeFromLink(link, param) + }) + } + + removeFromLink (link, param) { + const url = new URL(link.href) + url.searchParams.delete(param) + link.href = url + } } diff --git a/app/models/authorization_request/api_impot_particulier.rb b/app/models/authorization_request/api_impot_particulier.rb index fabe248a3..89ddc7f6e 100644 --- a/app/models/authorization_request/api_impot_particulier.rb +++ b/app/models/authorization_request/api_impot_particulier.rb @@ -6,8 +6,8 @@ class AuthorizationRequest::APIImpotParticulier < AuthorizationRequest include AuthorizationExtensions::OperationalAcceptance include AuthorizationExtensions::SafetyCertification include AuthorizationExtensions::Volumetrie - include DGFIPExtensions::APIImpotParticulierScopes + include DGFIPExtensions::APIImpotParticulierModalities VOLUMETRIES = { '50 appels / minute': 50, diff --git a/app/models/authorization_request/api_impot_particulier_sandbox.rb b/app/models/authorization_request/api_impot_particulier_sandbox.rb index 3d5ec1633..6d9885871 100644 --- a/app/models/authorization_request/api_impot_particulier_sandbox.rb +++ b/app/models/authorization_request/api_impot_particulier_sandbox.rb @@ -3,8 +3,8 @@ class AuthorizationRequest::APIImpotParticulierSandbox < AuthorizationRequest include AuthorizationExtensions::PersonalData include AuthorizationExtensions::CadreJuridique include AuthorizationExtensions::GDPRContacts - include DGFIPExtensions::APIImpotParticulierScopes + include DGFIPExtensions::APIImpotParticulierModalities add_document :maquette_projet, content_type: ['application/pdf'], size: { less_than: 10.megabytes } diff --git a/app/models/concerns/authorization_extensions/modalities.rb b/app/models/concerns/authorization_extensions/modalities.rb new file mode 100644 index 000000000..1ad75a0e4 --- /dev/null +++ b/app/models/concerns/authorization_extensions/modalities.rb @@ -0,0 +1,28 @@ +module AuthorizationExtensions::Modalities + extend ActiveSupport::Concern + + included do + add_attributes :modalities + + validates :modalities, + presence: true, + if: -> { need_complete_validation?(:modalities) } + + validate :modalities_in_available_values, + if: -> { need_complete_validation?(:modalities) } + end + + def modalities_in_available_values + if modalities.is_a? Array + errors.add(:modalities) unless (modalities - available_modalities).empty? + elsif available_modalities.exclude?(modalities) + errors.add(:modalities) + end + end + + def available_modalities + self.class::MODALITIES + rescue NameError + raise "Must declare a constant MODALITIES in the model #{self.class}, for example %w[with_france_connect with_spi]" + end +end diff --git a/app/models/concerns/dgfip_extensions/api_impot_particulier_modalities.rb b/app/models/concerns/dgfip_extensions/api_impot_particulier_modalities.rb new file mode 100644 index 000000000..7ce6966e9 --- /dev/null +++ b/app/models/concerns/dgfip_extensions/api_impot_particulier_modalities.rb @@ -0,0 +1,22 @@ +module DGFIPExtensions::APIImpotParticulierModalities + extend ActiveSupport::Concern + + MODALITIES = %w[with_france_connect with_spi with_etat_civil].freeze + + included do + include AuthorizationExtensions::Modalities + + add_attribute :france_connect_authorization_id + + validates :france_connect_authorization_id, + presence: true, + inclusion: { in: ->(authorization_request) { authorization_request.organization.valid_authorizations_of(AuthorizationRequest::FranceConnect).pluck(:id).map(&:to_s) } }, + if: -> { modalities == 'with_france_connect' && need_complete_validation?(:modalities) } + end + + def associated_france_connect_authorization + return nil if france_connect_authorization_id.blank? + + Authorization.find(france_connect_authorization_id) + end +end diff --git a/app/views/authorization_request_forms/_form.html.erb b/app/views/authorization_request_forms/_form.html.erb index 2f8f22b6b..6bba708ed 100644 --- a/app/views/authorization_request_forms/_form.html.erb +++ b/app/views/authorization_request_forms/_form.html.erb @@ -17,6 +17,8 @@ <% end %> <%= yield %> + + <%= render partial: "authorization_request_forms/shared/hidden_attributes", locals: { f: f } %> <% if content_for? :sticky_bar %> diff --git a/app/views/authorization_request_forms/build/modalities.html.erb b/app/views/authorization_request_forms/build/modalities.html.erb new file mode 100644 index 000000000..5bb693b70 --- /dev/null +++ b/app/views/authorization_request_forms/build/modalities.html.erb @@ -0,0 +1,4 @@ +<%= authorization_request_form(@authorization_request) do |f| %> + <%= render partial: 'authorization_request_forms/shared/modalities', locals: { f: } %> + <%= render partial: 'authorization_request_forms/build/wizard_buttons', locals: { f: } %> +<% end %> diff --git a/app/views/authorization_request_forms/shared/_hidden_attributes.html.erb b/app/views/authorization_request_forms/shared/_hidden_attributes.html.erb new file mode 100644 index 000000000..6e4199009 --- /dev/null +++ b/app/views/authorization_request_forms/shared/_hidden_attributes.html.erb @@ -0,0 +1,5 @@ +<% if hidden_attributes.present? && hidden_attributes[:attributes].present? %> + <% hidden_attributes[:attributes].each do |key, value| %> + <%= f.hidden_field key, value: %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/authorization_request_forms/shared/_modalities.html.erb b/app/views/authorization_request_forms/shared/_modalities.html.erb index d149a4e1c..9ae4a83bf 100644 --- a/app/views/authorization_request_forms/shared/_modalities.html.erb +++ b/app/views/authorization_request_forms/shared/_modalities.html.erb @@ -1 +1,42 @@ - +<% if @authorization_request.display_prefilled_banner_for_each_block? && @authorization_request.prefilled_data?(%i[modalities france_connect_authorization_id]) %> + <%= render partial: "authorization_request_forms/shared/prefilled_banner" %> +<% end %> + +<%= f.info_for(:modalities) %> + +
<%= t("authorization_requests.new.modalite.subtitle").html_safe %>
+<%= t("authorization_request_forms.api_impot_particulier_sandbox.modalities.subtitle").html_safe %>
- <%= t("authorization_requests.new.modalite.callout.content") %> + <%= t("authorization_request_forms.api_impot_particulier_sandbox.modalities.callout.content") %>
- <%= link_to t("authorization_requests.new.modalite.callout.link"), new_authorization_request_form_path("france-connect"), class: "fr-link fr-btn--icon-right fr-icon-arrow-right-line" %> + <%= link_to t("authorization_request_forms.api_impot_particulier_sandbox.modalities.callout.link"), new_authorization_request_form_path("france-connect"), class: "fr-link fr-btn--icon-right fr-icon-arrow-right-line" %>+ <%= @authorization_request.associated_france_connect_authorization.full_name %> +
+ <% end %> <% end %> diff --git a/config/authorization_definitions.yml b/config/authorization_definitions.yml index 5898b6c6c..bb8a382aa 100644 --- a/config/authorization_definitions.yml +++ b/config/authorization_definitions.yml @@ -344,6 +344,7 @@ shared: - name: basic_infos - name: personal_data - name: legal + - name: modalities - name: scopes - name: contacts - name: operational_acceptance @@ -486,6 +487,7 @@ shared: - name: basic_infos - name: personal_data - name: legal + - name: modalities - name: scopes - name: contacts scopes: *api_impot_particulier_scopes diff --git a/config/authorization_request_forms/api_impot_particulier.yml b/config/authorization_request_forms/api_impot_particulier.yml index 0cb457d3c..a34045db9 100644 --- a/config/authorization_request_forms/api_impot_particulier.yml +++ b/config/authorization_request_forms/api_impot_particulier.yml @@ -34,6 +34,7 @@ api-impot-particulier-sandbox: - name: basic_infos - name: personal_data - name: legal + - name: modalities - name: scopes - name: contacts @@ -84,6 +85,7 @@ api-impot-particulier-editeur: - name: basic_infos - name: personal_data - name: legal + - name: modalities - name: scopes - name: contacts - name: operational_acceptance diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index 6ab61402b..12054c9a2 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -53,6 +53,10 @@ fr: volumetrie_appels_par_minute: La limitation de débit volumetrie_justification: La justification de la limitation de débit + modalities: La modalité d'accès aux données + france_connect_authorization_id: L'habilitation FranceConnect + + scopes: Les données specific_requirements: Expression de besoin spécifique specific_requirements_document: Document de l'expression de besoin spécifique @@ -118,6 +122,8 @@ fr: blank: "ne sont pas cochées : il faut au moins qu'une des données soit sélectionnée." safety_certification_begin_date: comparison: doit être supérieure à la date de début + france_connect_authorization_id: + inclusion: n'est pas incluse dans la liste des habilitations validées de votre organization ransack: attributes: authorization_request: diff --git a/config/locales/authorization_request_forms.fr.yml b/config/locales/authorization_request_forms.fr.yml index 2fe34127b..a09d5e402 100644 --- a/config/locales/authorization_request_forms.fr.yml +++ b/config/locales/authorization_request_forms.fr.yml @@ -112,6 +112,10 @@ fr: description: Vous pouvez choisir d'indiquer une URL ou d'ajouter un fichier cadre_juridique_url: Veuillez nous indiquer l'url du texte relatif au traitement des données personnelles + modalities: + title: Comment vos usagers accèderont aux données ? + subtitle: Cette API est FranceConnectée, vos utilisateurs peuvent donc utiliser FranceConnect pour vous transmettre directement les données dont vous avez besoin + scopes: title: Quelles sont les données dont vous avez besoin ? subtitle: Soyez raisonnable, ne demandez l’accès à une donnée que si elle est vraiment utile pour votre projet. @@ -659,6 +663,25 @@ fr: hint: Cette information permettra de créer une équipe via la Régie et ainsi permettre à plusieurs personnes de votre équipe d'utiliser les fonctionnalités protégées de l'espace agent api_impot_particulier_sandbox: &api_impot_particulier_sandbox + modalities: + title: Comment vos usagers accèderont aux données ? + subtitle: Cette API est FranceConnectée, vos utilisateurs peuvent donc utiliser FranceConnect pour vous transmettre directement les données dont vous avez besoin + label: Modalité d'accès aux données + values: + with_france_connect: + label: Avec FranceConnect + with_spi: + label: Via le numéro fiscal (SPI) + with_etat_civil: + label: Via l'état civil + callout: + title: Pour proposer FranceConnect à vos utilisateurs, il vous faudra au préalable demander une habilitation FranceConnect. + content: Vous pourrez poursuivre votre demande d’habilitation à l’API Impôt Particulier lorsque votre habilitation FranceConnect sera validée. + link: Demander une habilitation FranceConnect + + france_connect_authorization_id: + label: Sélectionnez une habilitation FranceConnect qui sera liée à cette demande + scopes: info: title: Mode de fonctionnement de l’API Impôt particulier diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 947653d4a..16ce39210 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -10,6 +10,7 @@ fr: basic_infos: informations-de-base personal_data: donnees-personnelles legal: cadre-juridique + modalities: modalites-d-appel scopes: donnees contacts: contacts safety_certification: homologation-de-securite @@ -234,22 +235,6 @@ fr: