Merge pull request #7585 from betagouv/feat-services-with-siret

feat(services): add siret + geoloc
This commit is contained in:
Colin Darie 2022-09-06 09:30:07 +02:00 committed by GitHub
commit 05dfc93b7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 634 additions and 128 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1990,6 +1990,7 @@ type Service {
id: ID!
nom: String!
organisme: String!
siret: String
typeOrganisme: TypeOrganisme!
}

View file

@ -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

View file

@ -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}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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'

View file

@ -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"

View file

@ -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'

View file

@ -0,0 +1,5 @@
class AddSiretToServices < ActiveRecord::Migration[6.1]
def change
add_column :services, :siret, :string
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
]
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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