diff --git a/app/controllers/administrateurs/services_controller.rb b/app/controllers/administrateurs/services_controller.rb index 91ad55375..1126f29f7 100644 --- a/app/controllers/administrateurs/services_controller.rb +++ b/app/controllers/administrateurs/services_controller.rb @@ -74,7 +74,7 @@ module Administrateurs private def service_params - params.require(:service).permit(:nom, :organisme, :type_organisme, :email, :telephone, :horaires, :adresse) + params.require(:service).permit(:nom, :organisme, :type_organisme, :email, :telephone, :horaires, :adresse, :siret) end def service diff --git a/app/dashboards/service_dashboard.rb b/app/dashboards/service_dashboard.rb index 36134116a..463733204 100644 --- a/app/dashboards/service_dashboard.rb +++ b/app/dashboards/service_dashboard.rb @@ -18,7 +18,10 @@ class ServiceDashboard < Administrate::BaseDashboard email: Field::String, telephone: Field::String, horaires: Field::String, - adresse: Field::String + adresse: Field::String, + siret: Field::String, + etablissement_adresse: Field::String.with_options(searchable: false), + etablissement_latlng: GeopointField }.freeze # COLLECTION_ATTRIBUTES @@ -44,7 +47,10 @@ class ServiceDashboard < Administrate::BaseDashboard :email, :telephone, :horaires, - :adresse + :adresse, + :siret, + :etablissement_adresse, + :etablissement_latlng ].freeze # FORM_ATTRIBUTES diff --git a/app/fields/geopoint_field.rb b/app/fields/geopoint_field.rb new file mode 100644 index 000000000..0182d1ddb --- /dev/null +++ b/app/fields/geopoint_field.rb @@ -0,0 +1,15 @@ +require "administrate/field/base" + +class GeopointField < Administrate::Field::Base + def lat + data.first + end + + def lng + data.last + end + + def present? + lat.present? && lng.present? + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 5e8d05227..30786f95c 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1990,6 +1990,7 @@ type Service { id: ID! nom: String! organisme: String! + siret: String typeOrganisme: TypeOrganisme! } diff --git a/app/graphql/types/service_type.rb b/app/graphql/types/service_type.rb index 71b8c7b0d..e70d9df27 100644 --- a/app/graphql/types/service_type.rb +++ b/app/graphql/types/service_type.rb @@ -11,5 +11,6 @@ module Types field :nom, String, null: false field :type_organisme, TypeOrganisme, null: false field :organisme, String, null: false + field :siret, String, null: true end end diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index 91c2b4360..3de889329 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -90,7 +90,7 @@ module DossierHelper l(dossier.expiration_date, format: '%d/%m/%Y') end - def annuaire_link(siren) + def annuaire_link(siren = nil) base_url = "https://annuaire-entreprises.data.gouv.fr" return base_url if siren.blank? "#{base_url}/entreprise/#{siren}" diff --git a/app/jobs/api_entreprise/service_job.rb b/app/jobs/api_entreprise/service_job.rb new file mode 100644 index 000000000..9a2d39b14 --- /dev/null +++ b/app/jobs/api_entreprise/service_job.rb @@ -0,0 +1,15 @@ +class APIEntreprise::ServiceJob < APIEntreprise::Job + def perform(service_id) + service = Service.find(service_id) + + service_params = APIEntreprise::ServiceAdapter.new(service.siret, service_id).to_params + service.etablissement_infos = service_params + + point = Geocoder.search(service_params[:adresse]).first + + service.etablissement_lat = point&.latitude + service.etablissement_lng = point&.longitude + + service.save! + end +end diff --git a/app/lib/api_entreprise/adapter.rb b/app/lib/api_entreprise/adapter.rb index 84655a9b2..6cb4411c2 100644 --- a/app/lib/api_entreprise/adapter.rb +++ b/app/lib/api_entreprise/adapter.rb @@ -6,6 +6,10 @@ class APIEntreprise::Adapter @procedure_id = procedure_id end + def api(procedure_id = nil) + APIEntreprise::API.new(procedure_id) + end + def data_source begin @data_source ||= get_resource diff --git a/app/lib/api_entreprise/api.rb b/app/lib/api_entreprise/api.rb index 9f92f5cad..4588ea4ab 100644 --- a/app/lib/api_entreprise/api.rb +++ b/app/lib/api_entreprise/api.rb @@ -13,54 +13,77 @@ class APIEntreprise::API TIMEOUT = 20 DEFAULT_API_ENTREPRISE_DELAY = 0.0 - def self.entreprise(siren, procedure_id) - call_with_siret(ENTREPRISE_RESOURCE_NAME, siren, procedure_id) + attr_reader :procedure + attr_accessor :token + attr_accessor :api_object + + def initialize(procedure_id = nil) + return if procedure_id.blank? + + @procedure = Procedure.find(procedure_id) + @token = @procedure.api_entreprise_token end - def self.etablissement(siret, procedure_id) - call_with_siret(ETABLISSEMENT_RESOURCE_NAME, siret, procedure_id) + def entreprise(siren) + call_with_siret(ENTREPRISE_RESOURCE_NAME, siren) end - def self.exercices(siret, procedure_id) - call_with_siret(EXERCICES_RESOURCE_NAME, siret, procedure_id) + def etablissement(siret) + call_with_siret(ETABLISSEMENT_RESOURCE_NAME, siret) end - def self.rna(siret, procedure_id) - call_with_siret(RNA_RESOURCE_NAME, siret, procedure_id) + def exercices(siret) + call_with_siret(EXERCICES_RESOURCE_NAME, siret) end - def self.effectifs(siren, procedure_id, annee, mois) + def rna(siret) + call_with_siret(RNA_RESOURCE_NAME, siret) + end + + def effectifs(siren, annee, mois) endpoint = [EFFECTIFS_RESOURCE_NAME, annee, mois, "entreprise"].join('/') - call_with_siret(endpoint, siren, procedure_id) + call_with_siret(endpoint, siren) end - def self.effectifs_annuels(siren, procedure_id) - call_with_siret(EFFECTIFS_ANNUELS_RESOURCE_NAME, siren, procedure_id) + def effectifs_annuels(siren) + call_with_siret(EFFECTIFS_ANNUELS_RESOURCE_NAME, siren) end - def self.attestation_sociale(siren, procedure_id) - procedure = Procedure.find(procedure_id) - call_with_siret(ATTESTATION_SOCIALE_RESOURCE_NAME, siren, procedure_id) if procedure.api_entreprise_role?("attestations_sociales") + def attestation_sociale(siren) + return unless procedure.api_entreprise_role?("attestations_sociales") + + call_with_siret(ATTESTATION_SOCIALE_RESOURCE_NAME, siren) end - def self.attestation_fiscale(siren, procedure_id, user_id) - procedure = Procedure.find(procedure_id) - call_with_siret(ATTESTATION_FISCALE_RESOURCE_NAME, siren, procedure_id, user_id) if procedure.api_entreprise_role?("attestations_fiscales") + def attestation_fiscale(siren, user_id) + return unless procedure.api_entreprise_role?("attestations_fiscales") + + call_with_siret(ATTESTATION_FISCALE_RESOURCE_NAME, siren, user_id: user_id) end - def self.bilans_bdf(siren, procedure_id) - procedure = Procedure.find(procedure_id) - call_with_siret(BILANS_BDF_RESOURCE_NAME, siren, procedure_id) if procedure.api_entreprise_role?("bilans_entreprise_bdf") + def bilans_bdf(siren) + return unless procedure.api_entreprise_role?("bilans_entreprise_bdf") + + call_with_siret(BILANS_BDF_RESOURCE_NAME, siren) end - def self.privileges(token) - call_with_token(PRIVILEGES_RESOURCE_NAME, token) + def privileges + url = make_url(PRIVILEGES_RESOURCE_NAME) + call(url) end private - def self.call_with_token(resource_name, token) - url = "#{API_ENTREPRISE_URL}/#{resource_name}" + def call_with_siret(resource_name, siret_or_siren, user_id: nil) + url = make_url(resource_name, siret_or_siren) + + params = build_params(user_id) + + call(url, params) + end + + def call(url, params = nil) + verify_token! # this is a poor man throttling # the idea is to queue api entreprise job on 1 worker @@ -72,32 +95,13 @@ class APIEntreprise::API response = Typhoeus.get(url, headers: { Authorization: "Bearer #{token}" }, - timeout: TIMEOUT) - - if response.success? - JSON.parse(response.body, symbolize_names: true) - else - raise RequestFailed.new(response) - end - end - - def self.call_with_siret(resource_name, siret_or_siren, procedure_id, user_id = nil) - if APIEntrepriseToken.new(token_for_procedure(procedure_id)).expired? - raise APIEntrepriseToken::TokenError, I18n.t("api_entreprise.errors.token_expired") - end - - url = url(resource_name, siret_or_siren) - params = params(siret_or_siren, procedure_id, user_id) - - if api_entreprise_delay != 0.0 - sleep api_entreprise_delay - end - - response = Typhoeus.get(url, - headers: { Authorization: "Bearer #{token_for_procedure(procedure_id)}" }, params: params, timeout: TIMEOUT) + handle_response(response) + end + + def handle_response(response) if response.success? JSON.parse(response.body, symbolize_names: true) elsif response.code&.between?(401, 499) @@ -115,29 +119,41 @@ class APIEntreprise::API end end - def self.url(resource_name, siret_or_siren) - [API_ENTREPRISE_URL, resource_name, siret_or_siren].join("/") + def make_url(resource_name, siret_or_siren = nil) + [API_ENTREPRISE_URL, resource_name, siret_or_siren].compact.join("/") end - def self.params(siret_or_siren, procedure_id, user_id) - # rubocop:disable DS/ApplicationName - params = { - context: "demarches-simplifiees.fr", - recipient: ENV.fetch('API_ENTREPRISE_DEFAULT_SIRET'), - object: "procedure_id: #{procedure_id}", - non_diffusables: true - } - # rubocop:enable DS/ApplicationName + def build_params(user_id) + params = base_params + + params[:object] = if api_object.present? + api_object + elsif procedure.present? + "procedure_id: #{procedure.id}" + end + params[:user_id] = user_id if user_id.present? + params end - def self.token_for_procedure(procedure_id) - procedure = Procedure.find(procedure_id) - procedure.api_entreprise_token + def base_params + # rubocop:disable DS/ApplicationName + { + context: "demarches-simplifiees.fr", + recipient: ENV.fetch('API_ENTREPRISE_DEFAULT_SIRET'), + non_diffusables: true + } + # rubocop:enable DS/ApplicationName end - def self.api_entreprise_delay + def api_entreprise_delay ENV.fetch("API_ENTREPRISE_DELAY", DEFAULT_API_ENTREPRISE_DELAY).to_f end + + def verify_token! + return unless APIEntrepriseToken.new(token).expired? + + raise APIEntrepriseToken::TokenError, I18n.t("api_entreprise.errors.token_expired") + end end diff --git a/app/lib/api_entreprise/attestation_fiscale_adapter.rb b/app/lib/api_entreprise/attestation_fiscale_adapter.rb index e776dc48d..885e74956 100644 --- a/app/lib/api_entreprise/attestation_fiscale_adapter.rb +++ b/app/lib/api_entreprise/attestation_fiscale_adapter.rb @@ -8,7 +8,7 @@ class APIEntreprise::AttestationFiscaleAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.attestation_fiscale(siren, @procedure_id, @user_id) + api(@procedure_id).attestation_fiscale(siren, @user_id) end def process_params diff --git a/app/lib/api_entreprise/attestation_sociale_adapter.rb b/app/lib/api_entreprise/attestation_sociale_adapter.rb index 551b042b2..0aad5b18d 100644 --- a/app/lib/api_entreprise/attestation_sociale_adapter.rb +++ b/app/lib/api_entreprise/attestation_sociale_adapter.rb @@ -7,7 +7,7 @@ class APIEntreprise::AttestationSocialeAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.attestation_sociale(siren, @procedure_id) + api(@procedure_id).attestation_sociale(siren) end def process_params diff --git a/app/lib/api_entreprise/bilans_bdf_adapter.rb b/app/lib/api_entreprise/bilans_bdf_adapter.rb index 7e8de8932..c50d689b6 100644 --- a/app/lib/api_entreprise/bilans_bdf_adapter.rb +++ b/app/lib/api_entreprise/bilans_bdf_adapter.rb @@ -7,7 +7,7 @@ class APIEntreprise::BilansBdfAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.bilans_bdf(siren, @procedure_id) + api(@procedure_id).bilans_bdf(siren) end def process_params diff --git a/app/lib/api_entreprise/effectifs_adapter.rb b/app/lib/api_entreprise/effectifs_adapter.rb index 55c31e853..55011d6ac 100644 --- a/app/lib/api_entreprise/effectifs_adapter.rb +++ b/app/lib/api_entreprise/effectifs_adapter.rb @@ -9,7 +9,7 @@ class APIEntreprise::EffectifsAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.effectifs(siren, @procedure_id, @annee, @mois) + api(@procedure_id).effectifs(siren, @annee, @mois) end def process_params diff --git a/app/lib/api_entreprise/effectifs_annuels_adapter.rb b/app/lib/api_entreprise/effectifs_annuels_adapter.rb index 0dba6ae4b..62df2607e 100644 --- a/app/lib/api_entreprise/effectifs_annuels_adapter.rb +++ b/app/lib/api_entreprise/effectifs_annuels_adapter.rb @@ -7,7 +7,7 @@ class APIEntreprise::EffectifsAnnuelsAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.effectifs_annuels(siren, @procedure_id) + api(@procedure_id).effectifs_annuels(siren) end def process_params diff --git a/app/lib/api_entreprise/entreprise_adapter.rb b/app/lib/api_entreprise/entreprise_adapter.rb index 90176e21d..2c1ed0cb1 100644 --- a/app/lib/api_entreprise/entreprise_adapter.rb +++ b/app/lib/api_entreprise/entreprise_adapter.rb @@ -2,7 +2,7 @@ class APIEntreprise::EntrepriseAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.entreprise(siren, @procedure_id) + api(@procedure_id).entreprise(siren) end def process_params diff --git a/app/lib/api_entreprise/etablissement_adapter.rb b/app/lib/api_entreprise/etablissement_adapter.rb index eb8312d59..95eb20425 100644 --- a/app/lib/api_entreprise/etablissement_adapter.rb +++ b/app/lib/api_entreprise/etablissement_adapter.rb @@ -2,7 +2,7 @@ class APIEntreprise::EtablissementAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.etablissement(@siret, @procedure_id) + api(@procedure_id).etablissement(@siret) end def process_params diff --git a/app/lib/api_entreprise/exercices_adapter.rb b/app/lib/api_entreprise/exercices_adapter.rb index dc2cbd45f..f983e069d 100644 --- a/app/lib/api_entreprise/exercices_adapter.rb +++ b/app/lib/api_entreprise/exercices_adapter.rb @@ -2,7 +2,7 @@ class APIEntreprise::ExercicesAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.exercices(@siret, @procedure_id) + api(@procedure_id).exercices(@siret) end def process_params diff --git a/app/lib/api_entreprise/privileges_adapter.rb b/app/lib/api_entreprise/privileges_adapter.rb index 745102b9d..7c4d9ad76 100644 --- a/app/lib/api_entreprise/privileges_adapter.rb +++ b/app/lib/api_entreprise/privileges_adapter.rb @@ -15,6 +15,8 @@ class APIEntreprise::PrivilegesAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.privileges(@token) + api.tap do + _1.token = @token + end.privileges end end diff --git a/app/lib/api_entreprise/rna_adapter.rb b/app/lib/api_entreprise/rna_adapter.rb index a616cb19a..3aa7385fb 100644 --- a/app/lib/api_entreprise/rna_adapter.rb +++ b/app/lib/api_entreprise/rna_adapter.rb @@ -2,7 +2,7 @@ class APIEntreprise::RNAAdapter < APIEntreprise::Adapter private def get_resource - APIEntreprise::API.rna(@siret, @procedure_id) + api(@procedure_id).rna(@siret) end def process_params diff --git a/app/lib/api_entreprise/service_adapter.rb b/app/lib/api_entreprise/service_adapter.rb new file mode 100644 index 000000000..a37fa553a --- /dev/null +++ b/app/lib/api_entreprise/service_adapter.rb @@ -0,0 +1,23 @@ +class APIEntreprise::ServiceAdapter < APIEntreprise::EtablissementAdapter + def initialize(siret, service_id) + @siret = siret + @service_id = service_id + end + + private + + def get_resource + api_instance = api + # TODO: reuse instead a token from an administrateur's procedure? + api_instance.token = Rails.application.secrets.api_entreprise[:key] + api_instance.api_object = "service_id: #{@service_id}" + api_instance.etablissement(@siret) + end + + def attr_to_fetch + [ + :adresse, + :siret + ] + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 062893d03..bac400e41 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,17 +2,21 @@ # # Table name: services # -# id :bigint not null, primary key -# adresse :text -# email :string -# horaires :text -# nom :string not null -# organisme :string -# telephone :string -# type_organisme :string not null -# created_at :datetime not null -# updated_at :datetime not null -# administrateur_id :bigint +# id :bigint not null, primary key +# adresse :text +# email :string +# etablissement_infos :jsonb +# etablissement_lat :decimal(10, 6) +# etablissement_lng :decimal(10, 6) +# horaires :text +# nom :string not null +# organisme :string +# siret :string +# telephone :string +# type_organisme :string not null +# created_at :datetime not null +# updated_at :datetime not null +# administrateur_id :bigint # class Service < ApplicationRecord has_many :procedures @@ -33,6 +37,7 @@ class Service < ApplicationRecord validates :nom, presence: { message: 'doit être renseigné' }, allow_nil: false validates :nom, uniqueness: { scope: :administrateur, message: 'existe déjà' } validates :organisme, presence: { message: 'doit être renseigné' }, allow_nil: false + validates :siret, siret_format: true validates :type_organisme, presence: { message: 'doit être renseigné' }, allow_nil: false validates :email, presence: { message: 'doit être renseigné' }, allow_nil: false validates :telephone, phone: { possible: true, allow_blank: true } @@ -40,6 +45,8 @@ class Service < ApplicationRecord validates :adresse, presence: { message: 'doit être renseignée' }, allow_nil: false validates :administrateur, presence: { message: 'doit être renseigné' }, allow_nil: false + after_commit :enqueue_api_entreprise, if: -> { siret_previously_changed? } + def clone_and_assign_to_administrateur(administrateur) service_cloned = self.dup service_cloned.administrateur = administrateur @@ -51,4 +58,18 @@ class Service < ApplicationRecord "tel:#{telephone.gsub(/[[:blank:]]/, '')}" end end + + def etablissement_adresse + etablissement_infos.fetch("adresse", nil) + end + + def etablissement_latlng + [etablissement_lat, etablissement_lng] + end + + private + + def enqueue_api_entreprise + APIEntreprise::ServiceJob.perform_later(self.id) + end end diff --git a/app/serializers/service_serializer.rb b/app/serializers/service_serializer.rb index 460c580df..12586202c 100644 --- a/app/serializers/service_serializer.rb +++ b/app/serializers/service_serializer.rb @@ -6,4 +6,5 @@ class ServiceSerializer < ActiveModel::Serializer attribute :telephone, key: :phone attribute :horaires, key: :schedule attribute :adresse, key: :address + attribute :siret, key: :siret end diff --git a/app/validators/siret_format_validator.rb b/app/validators/siret_format_validator.rb index 7b0e8c750..964dd9e4a 100644 --- a/app/validators/siret_format_validator.rb +++ b/app/validators/siret_format_validator.rb @@ -2,9 +2,7 @@ class SiretFormatValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if !format_is_valid(value) record.errors.add(attribute, :length) - end - - if !luhn_passed(value) + elsif !luhn_passed(value) record.errors.add(attribute, :checksum) end end @@ -14,7 +12,7 @@ class SiretFormatValidator < ActiveModel::EachValidator LA_POSTE_SIREN = '356000000' def format_is_valid(value) - value.match?(/^\d{14}$/) + value&.match?(/^\d{14}$/) end def luhn_passed(value) diff --git a/app/views/administrateurs/services/_form.html.haml b/app/views/administrateurs/services/_form.html.haml index 8b011888f..c11e2a2ce 100644 --- a/app/views/administrateurs/services/_form.html.haml +++ b/app/views/administrateurs/services/_form.html.haml @@ -20,6 +20,16 @@ = f.select :type_organisme, Service.type_organismes.keys.map { |key| [ I18n.t("type_organisme.#{key}"), key] }, class: 'width-33-desktop width-100-mobile' + = f.label :siret do + Numéro SIRET + %span.mandatory * + + %p.notice + Veuillez saisir le numéro de SIRET de l'organisme dont ce service dépend. + %br + = link_to "➡ Rechercher le numéro SIRET sur « annuaire-entreprises.data.gouv.fr »", annuaire_link, target: "_blank" + + = f.text_field :siret, placeholder: "14 chiffres", required: true = render Dsfr::CalloutComponent.new(title: "Informations de contact") do |c| - c.body do diff --git a/app/views/fields/geopoint_field/_show.html.haml b/app/views/fields/geopoint_field/_show.html.haml new file mode 100644 index 000000000..d31a58a19 --- /dev/null +++ b/app/views/fields/geopoint_field/_show.html.haml @@ -0,0 +1,2 @@ +- if field.present? + = link_to field.data.join(", "), "https://www.geoportail.gouv.fr/carte?c=#{field.lng},#{field.lat}&z=17&permalink=yes", target: '_blank', rel: 'noopener' diff --git a/config/locales/models/service/en.yml b/config/locales/models/service/en.yml index 5d3012f12..a28cd43a5 100644 --- a/config/locales/models/service/en.yml +++ b/config/locales/models/service/en.yml @@ -9,3 +9,11 @@ en: adresse: 'Mail address' email: 'Email' telephone: 'Phone' + errors: + models: + service: + attributes: + siret: + format: "SIRET number %{message}" + length: "must contain exactly 14 digits" + checksum: "is invalid" diff --git a/config/locales/models/service/fr.yml b/config/locales/models/service/fr.yml index c1e7fed72..e7158667e 100644 --- a/config/locales/models/service/fr.yml +++ b/config/locales/models/service/fr.yml @@ -9,6 +9,14 @@ fr: adresse: 'Adresse postale' email: 'Email de contact' telephone: 'Téléphone' + errors: + models: + service: + attributes: + siret: + format: "Le numéro SIRET %{message}" + length: "doit comporter exactement 14 chiffres" + checksum: "est invalide" type_organisme: administration_centrale: 'Administration centrale' association: 'Association' diff --git a/db/migrate/20220718134835_add_siret_to_services.rb b/db/migrate/20220718134835_add_siret_to_services.rb new file mode 100644 index 000000000..b86ed3073 --- /dev/null +++ b/db/migrate/20220718134835_add_siret_to_services.rb @@ -0,0 +1,5 @@ +class AddSiretToServices < ActiveRecord::Migration[6.1] + def change + add_column :services, :siret, :string + end +end diff --git a/db/migrate/20220725090224_add_etablissement_infos_to_services.rb b/db/migrate/20220725090224_add_etablissement_infos_to_services.rb new file mode 100644 index 000000000..21534c008 --- /dev/null +++ b/db/migrate/20220725090224_add_etablissement_infos_to_services.rb @@ -0,0 +1,14 @@ +class AddEtablissementInfosToServices < ActiveRecord::Migration[6.1] + def up + add_column :services, :etablissement_infos, :jsonb + add_column :services, :etablissement_lat, :decimal, precision: 10, scale: 6 + add_column :services, :etablissement_lng, :decimal, precision: 10, scale: 6 + change_column_default :services, :etablissement_infos, {} + end + + def down + remove_column :services, :etablissement_infos + remove_column :services, :etablissement_lat + remove_column :services, :etablissement_lng + end +end diff --git a/db/migrate/20220725090225_backfill_add_etablissement_infos_to_services.rb b/db/migrate/20220725090225_backfill_add_etablissement_infos_to_services.rb new file mode 100644 index 000000000..f5f5a1eb2 --- /dev/null +++ b/db/migrate/20220725090225_backfill_add_etablissement_infos_to_services.rb @@ -0,0 +1,10 @@ +class BackfillAddEtablissementInfosToServices < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def up + Service.unscoped.in_batches do |relation| # rubocop:disable DS/Unscoped + relation.update_all etablissement_infos: {} + sleep(0.01) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 361097d7d..33115f0cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -706,9 +706,13 @@ ActiveRecord::Schema.define(version: 2022_09_02_151920) do t.text "adresse" t.datetime "created_at", null: false t.string "email" + t.jsonb "etablissement_infos", default: {} + t.decimal "etablissement_lat", precision: 10, scale: 6 + t.decimal "etablissement_lng", precision: 10, scale: 6 t.text "horaires" t.string "nom", null: false t.string "organisme" + t.string "siret" t.string "telephone" t.string "type_organisme", null: false t.datetime "updated_at", null: false diff --git a/spec/controllers/administrateurs/services_controller_spec.rb b/spec/controllers/administrateurs/services_controller_spec.rb index e4070f9ed..71c075ae9 100644 --- a/spec/controllers/administrateurs/services_controller_spec.rb +++ b/spec/controllers/administrateurs/services_controller_spec.rb @@ -18,9 +18,10 @@ describe Administrateurs::ServicesController, type: :controller do email: 'email@toto.com', telephone: '1234', horaires: 'horaires', - adresse: 'adresse' + adresse: 'adresse', + siret: "35600082800018" }, - procedure_id: 12 + procedure_id: procedure.id } end @@ -33,7 +34,8 @@ describe Administrateurs::ServicesController, type: :controller do it { expect(Service.last.telephone).to eq('1234') } it { expect(Service.last.horaires).to eq('horaires') } it { expect(Service.last.adresse).to eq('adresse') } - it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) } + it { expect(Service.last.siret).to eq('35600082800018') } + it { expect(response).to redirect_to(admin_services_path(procedure_id: procedure.id)) } end context 'when submitting an invalid service' do @@ -113,13 +115,13 @@ describe Administrateurs::ServicesController, type: :controller do context 'when a service has no related procedure' do before do sign_in(admin.user) - delete :destroy, params: { id: service.id, procedure_id: 12 } + delete :destroy, params: { id: service.id, procedure_id: procedure.id } end it { expect { service.reload }.to raise_error(ActiveRecord::RecordNotFound) } it { expect(flash.alert).to be_nil } it { expect(flash.notice).to eq("#{service.nom} est supprimé") } - it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) } + it { expect(response).to redirect_to(admin_services_path(procedure_id: procedure.id)) } end context 'when a service still has some related procedures' do @@ -127,13 +129,13 @@ describe Administrateurs::ServicesController, type: :controller do before do sign_in(admin.user) - delete :destroy, params: { id: service.id, procedure_id: 12 } + delete :destroy, params: { id: service.id, procedure_id: procedure.id } end it { expect(service.reload).not_to be_nil } it { expect(flash.alert).to eq("la démarche #{procedure.libelle} utilise encore le service #{service.nom}. Veuillez l'affecter à un autre service avant de pouvoir le supprimer") } it { expect(flash.notice).to be_nil } - it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) } + it { expect(response).to redirect_to(admin_services_path(procedure_id: procedure.id)) } end context "when a service has some related discarded procedures" do diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index 56e49be12..f5362b756 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -949,7 +949,7 @@ describe API::V2::GraphqlController do errors: nil }) - perform_enqueued_jobs + perform_enqueued_jobs except: [APIEntreprise::ServiceJob] expect(ActionMailer::Base.deliveries.size).to eq(4) end end @@ -992,7 +992,7 @@ describe API::V2::GraphqlController do errors: nil }) - perform_enqueued_jobs + perform_enqueued_jobs except: [APIEntreprise::ServiceJob] expect(ActionMailer::Base.deliveries.size).to eq(3) end end @@ -1035,7 +1035,7 @@ describe API::V2::GraphqlController do errors: nil }) - perform_enqueued_jobs + perform_enqueued_jobs except: [APIEntreprise::ServiceJob] expect(ActionMailer::Base.deliveries.size).to eq(4) end end @@ -1078,7 +1078,7 @@ describe API::V2::GraphqlController do errors: nil }) - perform_enqueued_jobs + perform_enqueued_jobs except: [APIEntreprise::ServiceJob] expect(ActionMailer::Base.deliveries.size).to eq(3) end end diff --git a/spec/controllers/manager/services_controller_spec.rb b/spec/controllers/manager/services_controller_spec.rb new file mode 100644 index 000000000..7c3b6b6a7 --- /dev/null +++ b/spec/controllers/manager/services_controller_spec.rb @@ -0,0 +1,31 @@ +describe Manager::ServicesController, type: :controller do + let(:super_admin) { create(:super_admin) } + before do + sign_in super_admin + @service = create(:service) + end + + render_views + + describe 'GET #index' do + it "should list services" do + get :index + expect(response.body).to include(@service.nom) + end + + it "should search by nom" do + get :index, params: { search: @service.nom.first(3) } + expect(response.body).to include(@service.nom) + end + end + + describe "GET #show" do + before do + get :show, params: { id: @service.id } + end + + it { expect(response.body).to include(@service.nom) } + it { expect(response.body).to include("75 rue du Louvre") } + it { expect(response.body).to have_link(href: "https://www.geoportail.gouv.fr/carte?c=2.34,48.87&z=17&permalink=yes") } + end +end diff --git a/spec/factories/service.rb b/spec/factories/service.rb index a3215328e..ea7502759 100644 --- a/spec/factories/service.rb +++ b/spec/factories/service.rb @@ -7,6 +7,10 @@ FactoryBot.define do telephone { '1234' } horaires { 'de 9 h à 18 h' } adresse { 'adresse' } + siret { '35600082800018' } + etablissement_infos { { adresse: "75 rue du Louvre\n75002\nPARIS\nFRANCE" } } + etablissement_lat { 48.87 } + etablissement_lng { 2.34 } association :administrateur end diff --git a/spec/fixtures/files/api_entreprise/privileges.json b/spec/fixtures/files/api_entreprise/privileges.json new file mode 100644 index 000000000..54f9def82 --- /dev/null +++ b/spec/fixtures/files/api_entreprise/privileges.json @@ -0,0 +1,29 @@ +{ + "privileges": [ + "attestations_agefiph", + "attestations_fiscales", + "attestations_sociales", + "certificat_cnetp", + "associations", + "certificat_opqibi", + "documents_association", + "etablissements", + "entreprises", + "extrait_court_inpi", + "extraits_rcs", + "exercices", + "fntp_carte_pro", + "qualibat", + "probtp", + "msa_cotisations", + "bilans_entreprise_bdf", + "certificat_rge_ademe", + "conventions_collectives", + "actes_inpi", + "bilans_inpi", + "effectifs_acoss", + "entreprises_artisanales", + "eori_douanes", + "certificat_agence_bio" + ] +} diff --git a/spec/jobs/api_entreprise/service_job_spec.rb b/spec/jobs/api_entreprise/service_job_spec.rb new file mode 100644 index 000000000..0e009eab3 --- /dev/null +++ b/spec/jobs/api_entreprise/service_job_spec.rb @@ -0,0 +1,72 @@ +RSpec.describe APIEntreprise::ServiceJob, type: :job do + let(:siret) { '41816609600051' } + let(:service) { create(:service, siret: siret) } + let(:entreprise_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } + let(:geocoder_body) { File.read('spec/fixtures/files/api_address/address.json') } + let(:status) { 200 } + + let (:adresse) { "OCTO TECHNOLOGY\r\n50 AVENUE DES CHAMPS ELYSEES\r\n75008 PARIS\r\nFRANCE" } + + before do + stub_request(:get, %r{https://entreprise.api.gouv.fr/v2/etablissements/}) + .to_return(body: entreprise_body, status: status) + allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(false) + + Geocoder.configure(lookup: :ban_data_gouv_fr, use_https: true) + + stub_request(:get, "https://api-adresse.data.gouv.fr/search/?q=#{adresse}") + .to_return(body: geocoder_body, status: status) + end + + after do + Geocoder.configure(lookup: :test) + end + + subject { described_class.new.perform(service.id) } + + it "update service with address" do + subject + infos = service.reload.etablissement_infos + + expect(infos).not_to be_empty + expect(infos["adresse"]).to eq(adresse) + expect(infos["numero_voie"]).to eq("50") + expect(infos["code_postal"]).to eq("75008") + expect(infos["localite"]).to eq("PARIS 8") + end + + it "geocode address" do + subject + service.reload + + expect(service.etablissement_lat).to eq(48.859) + expect(service.etablissement_lng).to eq(2.347) + end + + context "errors responses" do + it "clear attributes when no address match" do + stub_request(:get, %r{https://entreprise.api.gouv.fr/v2/etablissements/}) + .to_return(body: "{}", status: 404) + subject + service.reload + + expect(service.etablissement_infos).to be_empty + expect(service.etablissement_lat).to be_nil + expect(service.etablissement_lng).to be_nil + end + + it "supports empty geocode result" do + geocoder_response = JSON.parse(geocoder_body) + geocoder_response["features"] = [] + + stub_request(:get, "https://api-adresse.data.gouv.fr/search/?q=#{adresse}") + .to_return(body: JSON.generate(geocoder_response), status: status) + + subject + service.reload + + expect(service.etablissement_lat).to be_nil + expect(service.etablissement_lng).to be_nil + end + end +end diff --git a/spec/lib/api_entreprise/api_spec.rb b/spec/lib/api_entreprise/api_spec.rb index 03021f6f0..6aba1818b 100644 --- a/spec/lib/api_entreprise/api_spec.rb +++ b/spec/lib/api_entreprise/api_spec.rb @@ -4,7 +4,7 @@ describe APIEntreprise::API do let(:token) { Rails.application.secrets.api_entreprise[:key] } describe '.entreprise' do - subject { described_class.entreprise(siren, procedure_id) } + subject { described_class.new(procedure_id).entreprise(siren) } before do stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siren}/) @@ -85,7 +85,7 @@ describe APIEntreprise::API do end describe '.etablissement' do - subject { described_class.etablissement(siret, procedure_id) } + subject { described_class.new(procedure_id).etablissement(siret) } before do stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}?.*non_diffusables=true/) .to_return(status: status, body: body) @@ -121,7 +121,7 @@ describe APIEntreprise::API do end context 'when siret does not exist' do - subject { described_class.exercices(siret, procedure_id) } + subject { described_class.new(procedure_id).exercices(siret) } let(:siret) { '11111111111111' } let(:status) { 404 } @@ -133,7 +133,7 @@ describe APIEntreprise::API do end context 'when siret exists' do - subject { described_class.exercices(siret, procedure_id) } + subject { described_class.new(procedure_id).exercices(siret) } let(:siret) { '41816609600051' } let(:status) { 200 } @@ -152,7 +152,7 @@ describe APIEntreprise::API do allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(false) end - subject { described_class.rna(siren, procedure_id) } + subject { described_class.new(procedure_id).rna(siren) } context 'when siren does not exist' do let(:siren) { '111111111' } @@ -186,7 +186,7 @@ describe APIEntreprise::API do .to_return(body: body, status: status) end - subject { described_class.attestation_sociale(siren, procedure.id) } + subject { described_class.new(procedure.id).attestation_sociale(siren) } context 'when token not authorized' do let(:roles) { ["entreprises"] } @@ -215,7 +215,7 @@ describe APIEntreprise::API do .to_return(body: body, status: status) end - subject { described_class.attestation_fiscale(siren, procedure.id, user_id) } + subject { described_class.new(procedure.id).attestation_fiscale(siren, user_id) } context 'when token not authorized' do let(:roles) { ["entreprises"] } @@ -243,7 +243,7 @@ describe APIEntreprise::API do .to_return(body: body, status: status) end - subject { described_class.bilans_bdf(siren, procedure.id) } + subject { described_class.new(procedure.id).bilans_bdf(siren) } context 'when token not authorized' do let(:roles) { ["entreprises"] } @@ -258,9 +258,27 @@ describe APIEntreprise::API do end end + describe '.privileges' do + let(:api) { described_class.new } + let(:status) { 200 } + let(:body) { File.read('spec/fixtures/files/api_entreprise/privileges.json') } + subject { api.privileges } + + before do + api.token = token + + stub_request(:get, "https://entreprise.api.gouv.fr/v2/privileges") + .to_return(body: body, status: status) + end + + context 'when token is authorized' do + it { expect(subject).to eq(JSON.parse(body, symbolize_names: true)) } + end + end + describe 'with expired token' do let(:siren) { '111111111' } - subject { described_class.entreprise(siren, procedure_id) } + subject { described_class.new(procedure_id).entreprise(siren) } before do allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(true) diff --git a/spec/lib/api_entreprise/privileges_adapter_spec.rb b/spec/lib/api_entreprise/privileges_adapter_spec.rb new file mode 100644 index 000000000..6e72bf93b --- /dev/null +++ b/spec/lib/api_entreprise/privileges_adapter_spec.rb @@ -0,0 +1,24 @@ +describe APIEntreprise::PrivilegesAdapter do + let(:body) { File.read('spec/fixtures/files/api_entreprise/privileges.json') } + let(:status) { 200 } + let(:token) { "secret-token" } + let(:adapter) { described_class.new(token) } + + subject { adapter } + + before do + stub_request(:get, "https://entreprise.api.gouv.fr/v2/privileges") + .to_return(body: body, status: status) + allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(false) + end + + it { is_expected.to be_valid } + + context 'when token is not valid or missing' do + let(:token) { nil } + let(:status) { 403 } + let(:body) { '' } + + it { is_expected.not_to be_valid } + end +end diff --git a/spec/lib/api_entreprise/service_adapter_spec.rb b/spec/lib/api_entreprise/service_adapter_spec.rb new file mode 100644 index 000000000..e12a40c40 --- /dev/null +++ b/spec/lib/api_entreprise/service_adapter_spec.rb @@ -0,0 +1,89 @@ +describe APIEntreprise::ServiceAdapter do + before do + allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(false) + end + + let(:siret) { '41816609600051' } + let(:service) { create(:service, siret: siret) } + + context 'SIRET valide avec infos diffusables' do + let(:fixture) { 'spec/fixtures/files/api_entreprise/etablissements.json' } + subject { described_class.new(siret, service.id).to_params } + + before do + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) + .with(query: hash_including({ 'object' => "service_id: #{service.id}" })) + .to_return(body: File.read(fixture, status: 200)) + end + + it '#to_params class est une Hash ?' do + expect(subject).to be_a_instance_of(Hash) + end + + context 'Attributs Etablissements' do + it 'should contains a SIRET' do + expect(subject[:siret]).to eq(siret) + end + + it 'should not return siege_social information' do + expect(subject[:siege_social]).to be_nil + end + + context 'Concaténation lignes adresse' do + it 'service contains a multi lines adress' do + expect(subject[:adresse]).to eq("OCTO TECHNOLOGY\r\n50 AVENUE DES CHAMPS ELYSEES\r\n75008 PARIS\r\nFRANCE") + end + end + + context 'adress details' do + it 'service contains a numero_voie' do + expect(subject[:numero_voie]).to eq('50') + end + + it 'service contains a type_voie' do + expect(subject[:type_voie]).to eq('AV') + end + + it 'service contains a nom_voie' do + expect(subject[:nom_voie]).to eq('DES CHAMPS ELYSEES') + end + it 'service contains a complement_adresse' do + expect(subject[:complement_adresse]).to eq('complement_adresse') + end + + it 'service contains a code_postal' do + expect(subject[:code_postal]).to eq('75008') + end + + it 'service contains a localite' do + expect(subject[:localite]).to eq('PARIS 8') + end + + it 'service contains a code_insee_localite' do + expect(subject[:code_insee_localite]).to eq('75108') + end + end + end + + context 'Attributs Etablissements pour etablissement non siege' do + let(:siret) { '17310120500719' } + let(:fixture) { 'spec/fixtures/files/api_entreprise/etablissements-non-siege.json' } + it 'service contains a siret' do + expect(subject[:siret]).to eq(siret) + end + end + end + + context 'when siret is not found' do + let(:bad_siret) { 11_111_111_111_111 } + subject { described_class.new(bad_siret, service.id).to_params } + + before do + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{bad_siret}/) + .with(query: hash_including({ 'object' => "service_id: #{service.id}" })) + .to_return(body: 'Fake body', status: 404) + end + + it { expect(subject).to eq({}) } + end +end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index b8a4c479a..95abc24a6 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -10,36 +10,47 @@ describe Service, type: :model do telephone: '012345678', horaires: 'du lundi au vendredi', adresse: '12 rue des schtroumpfs', - administrateur_id: administrateur.id + administrateur_id: administrateur.id, + siret: "35600082800018" } end - it { expect(Service.new(params).valid?).to be_truthy } + subject { Service.new(params) } + + it { expect(Service.new(params)).to be_valid } it 'should forbid invalid phone numbers' do - service = Service.create(params) invalid_phone_numbers = ["1", "Néant", "01 60 50 40 30 20"] invalid_phone_numbers.each do |tel| - service.telephone = tel - expect(service.valid?).to be_falsey + subject.telephone = tel + expect(subject).not_to be_valid end end it 'should accept no phone numbers' do - service = Service.create(params) - service.telephone = nil - - expect(service.valid?).to be_truthy + subject.telephone = nil + expect(subject).to be_valid end it 'should accept valid phone numbers' do - service = Service.create(params) valid_phone_numbers = ["3646", "273115", "0160376983", "01 60 50 40 30 ", "+33160504030"] valid_phone_numbers.each do |tel| - service.telephone = tel - expect(service.valid?).to be_truthy + subject.telephone = tel + expect(subject).to be_valid + end + end + + describe "siret" do + it 'should not be invalid' do + subject.siret = "012345678901234" + expect(subject).not_to be_valid + end + + it 'should be required' do + subject.siret = nil + expect(subject).not_to be_valid end end @@ -47,25 +58,25 @@ describe Service, type: :model do before { Service.create(params) } context 'checks uniqueness of administrateur, name couple' do - it { expect(Service.create(params).valid?).to be_falsey } + it { expect(Service.create(params)).not_to be_valid } end end context 'of type_organisme' do it 'should be set' do - expect(Service.new(params.except(:type_organisme)).valid?).to be_falsey + expect(Service.new(params.except(:type_organisme))).not_to be_valid end end context 'of nom' do it 'should be set' do - expect(Service.new(params.except(:nom)).valid?).to be_falsey + expect(Service.new(params.except(:nom))).not_to be_valid end end context 'of administrateur' do it 'should be set' do - expect(Service.new(params.except(:administrateur_id)).valid?).to be_falsey + expect(Service.new(params.except(:administrateur_id))).not_to be_valid end end @@ -75,4 +86,66 @@ describe Service, type: :model do end end end + + describe "API Entreprise job" do + subject { create(:service) } + it "should enqueue a job when created" do + expect(APIEntreprise::ServiceJob).to have_been_enqueued.with(subject.id) + end + + it "should enqueue a job when siret changed" do + subject.update(siret: "35600082800018") + expect(APIEntreprise::ServiceJob).to have_been_enqueued.with(subject.id) + end + + it "should not enqueue a job when siret is unchanged" do + subject + clear_enqueued_jobs + subject.update(telephone: "09879789") + expect(APIEntreprise::ServiceJob).not_to have_been_enqueued + end + end + + describe "etablissement adresse & geo coordinates" do + subject { create(:service, etablissement_lat: latitude, etablissement_lng: longitude, etablissement_infos: etablissement_infos) } + + context "when the service has no geo coordinates" do + let(:latitude) { nil } + let(:longitude) { nil } + let(:etablissement_infos) { {} } + it "should return nil" do + expect(subject.etablissement_lat).to be_nil + expect(subject.etablissement_lng).to be_nil + expect(subject.etablissement_adresse).to be_nil + end + end + + context "when the service has geo coordinates" do + let(:latitude) { 43.5 } + let(:longitude) { 4.7 } + let(:adresse) { "174 Chemin du Beurre\n13200\nARLES\nFRANCE" } + let(:etablissement_infos) { { adresse: adresse } } + + it "should return nil" do + expect(subject.etablissement_lat).to eq(43.5) + expect(subject.etablissement_lng).to eq(4.7) + end + + it "should return etablissement adresse" do + expect(subject.etablissement_adresse).to eq(adresse) + end + end + end + + describe 'etablissement_latlng' do + it 'without coordinates' do + service = build(:service, etablissement_lat: nil, etablissement_lng: nil) + expect(service.etablissement_latlng).to eq([nil, nil]) + end + + it 'with coordinates' do + service = build(:service) + expect(service.etablissement_latlng).to eq([48.87, 2.34]) + end + end end