feat(service): prefill contact information from annuaire service public
This commit is contained in:
parent
a4054053f7
commit
8dc47c1b93
8 changed files with 462 additions and 0 deletions
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PrefillableFromServicePublicConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
def prefill_from_siret
|
||||
result = AnnuaireServicePublicService.new.(siret:)
|
||||
# TODO: get organisme, … from API Entreprise
|
||||
case result
|
||||
in Dry::Monads::Success(data)
|
||||
self.nom = data[:nom] if nom.blank?
|
||||
self.email = data[:adresse_courriel] if email.blank?
|
||||
self.telephone = data[:telephone]&.first&.dig("valeur") if telephone.blank?
|
||||
self.horaires = denormalize_plage_ouverture(data[:plage_ouverture]) if horaires.blank?
|
||||
self.adresse = APIGeoService.inline_service_public_address(data[:adresse]&.first) if adresse.blank?
|
||||
else
|
||||
# NOOP
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def denormalize_plage_ouverture(data)
|
||||
return if data.blank?
|
||||
|
||||
data.map do |range|
|
||||
day_range = range.values_at('nom_jour_debut', 'nom_jour_fin').uniq.join(' au ')
|
||||
|
||||
hours_range = (1..2).each_with_object([]) do |i, hours|
|
||||
start_hour = range["valeur_heure_debut_#{i}"]
|
||||
end_hour = range["valeur_heure_fin_#{i}"]
|
||||
|
||||
if start_hour.present? && end_hour.present?
|
||||
hours << "de #{format_time(start_hour)} à #{format_time(end_hour)}"
|
||||
end
|
||||
end
|
||||
|
||||
result = day_range
|
||||
result += " : #{hours_range.join(' et ')}" if hours_range.present?
|
||||
result += " (#{range['commentaire']})" if range['commentaire'].present?
|
||||
result
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def format_time(str_time)
|
||||
Time.zone
|
||||
.parse(str_time)
|
||||
.strftime("%-H:%M")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Service < ApplicationRecord
|
||||
include PrefillableFromServicePublicConcern
|
||||
|
||||
has_many :procedures
|
||||
belongs_to :administrateur, optional: false
|
||||
|
||||
|
|
76
app/schemas/service-public.json
Normal file
76
app/schemas/service-public.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "http://demarches-simplifiees.fr/service-public.schema.json",
|
||||
"title": "Service Public",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plage_ouverture": { "type": ["string", "null"] },
|
||||
"site_internet": { "type": ["string", "null"] },
|
||||
"copyright": { "type": ["string", "null"] },
|
||||
"siren": { "type": ["string", "null"] },
|
||||
"ancien_code_pivot": { "type": ["string", "null"] },
|
||||
"reseau_social": { "type": ["string", "null"] },
|
||||
"texte_reference": { "type": ["string", "null"] },
|
||||
"partenaire": { "type": ["string", "null"] },
|
||||
"telecopie": { "type": ["string", "null"] },
|
||||
"nom": { "type": "string" },
|
||||
"siret": { "type": ["string", "null"] },
|
||||
"itm_identifiant": { "type": ["string", "null"] },
|
||||
"sigle": { "type": ["string", "null"] },
|
||||
"affectation_personne": { "type": ["string", "null"] },
|
||||
"date_modification": { "type": "string" },
|
||||
"adresse_courriel": { "type": ["string", "null"] },
|
||||
"service_disponible": { "type": ["string", "null"] },
|
||||
"organigramme": { "type": ["string", "null"] },
|
||||
"pivot": { "type": ["string", "null"] },
|
||||
"partenaire_identifiant": { "type": ["string", "null"] },
|
||||
"ancien_identifiant": { "type": ["string", "null"] },
|
||||
"id": { "type": "string" },
|
||||
"ancien_nom": { "type": ["string", "null"] },
|
||||
"commentaire_plage_ouverture": { "type": ["string", "null"] },
|
||||
"annuaire": { "type": ["string", "null"] },
|
||||
"tchat": { "type": ["string", "null"] },
|
||||
"hierarchie": { "type": ["string", "null"] },
|
||||
"categorie": { "type": "string" },
|
||||
"sve": { "type": ["string", "null"] },
|
||||
"telephone_accessible": { "type": ["string", "null"] },
|
||||
"application_mobile": { "type": ["string", "null"] },
|
||||
"version_type": { "type": "string" },
|
||||
"type_repertoire": { "type": ["string", "null"] },
|
||||
"telephone": { "type": ["string", "null"] },
|
||||
"version_etat_modification": { "type": ["string", "null"] },
|
||||
"date_creation": { "type": "string" },
|
||||
"partenaire_date_modification": { "type": ["string", "null"] },
|
||||
"mission": { "type": ["string", "null"] },
|
||||
"formulaire_contact": { "type": ["string", "null"] },
|
||||
"version_source": { "type": ["string", "null"] },
|
||||
"type_organisme": { "type": ["string", "null"] },
|
||||
"code_insee_commune": { "type": ["string", "null"] },
|
||||
"statut_de_diffusion": { "type": ["string", "null"] },
|
||||
"adresse": { "type": ["string", "null"] },
|
||||
"url_service_public": { "type": ["string", "null"] },
|
||||
"information_complementaire": { "type": ["string", "null"] },
|
||||
"date_diffusion": { "type": ["string", "null"] }
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"nom",
|
||||
"categorie",
|
||||
"adresse",
|
||||
"adresse_courriel",
|
||||
"telephone",
|
||||
"plage_ouverture"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["total_count", "results"]
|
||||
}
|
52
app/services/annuaire_service_public_service.rb
Normal file
52
app/services/annuaire_service_public_service.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AnnuaireServicePublicService
|
||||
include Dry::Monads[:result]
|
||||
|
||||
def call(siret:)
|
||||
result = API::Client.new.call(url: url(siret), schema:, timeout: 1.second)
|
||||
|
||||
case result
|
||||
in Success(body:)
|
||||
result = body[:results].first
|
||||
|
||||
if result.present?
|
||||
Success(
|
||||
result.slice(:nom, :adresse, :adresse_courriel).merge(
|
||||
telephone: maybe_json_parse(result[:telephone]),
|
||||
plage_ouverture: maybe_json_parse(result[:plage_ouverture]),
|
||||
adresse: maybe_json_parse(result[:adresse])
|
||||
)
|
||||
)
|
||||
else
|
||||
Failure(API::Client::Error[:not_found, 404, false, "No result found for this SIRET."])
|
||||
end
|
||||
in Failure(code:, reason:) if code.in?(401..403)
|
||||
Sentry.capture_message("#{self.class.name}: #{reason} code: #{code}", extra: { siret: })
|
||||
Failure(API::Client::Error[:unauthorized, code, false, reason])
|
||||
in Failure(type: :schema, code:, reason:)
|
||||
reason.errors[0].first
|
||||
Sentry.capture_exception(reason, extra: { siret:, code: })
|
||||
|
||||
Failure(API::Client::Error[:schema, code, false, reason])
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schema
|
||||
JSONSchemer.schema(Rails.root.join('app/schemas/service-public.json'))
|
||||
end
|
||||
|
||||
def url(siret)
|
||||
"https://api-lannuaire.service-public.fr/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=siret:#{siret}"
|
||||
end
|
||||
|
||||
def maybe_json_parse(value)
|
||||
return nil if value.blank?
|
||||
|
||||
JSON.parse(value)
|
||||
end
|
||||
end
|
|
@ -263,6 +263,21 @@ class APIGeoService
|
|||
end
|
||||
end
|
||||
|
||||
def inline_service_public_address(address_data)
|
||||
return nil if address_data.blank?
|
||||
|
||||
components = [
|
||||
address_data['numero_voie'],
|
||||
address_data['complement1'],
|
||||
address_data['complement2'],
|
||||
address_data['service_distribution'],
|
||||
address_data['code_postal'],
|
||||
address_data['nom_commune']
|
||||
].compact_blank
|
||||
|
||||
components.join(' ')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def code_metropole?(result)
|
||||
|
|
70
spec/fixtures/cassettes/annuaire_service_public_failure_20004021000000.yml
vendored
Normal file
70
spec/fixtures/cassettes/annuaire_service_public_failure_20004021000000.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api-lannuaire.service-public.fr/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=siret:20004021000000
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- demarches.gouv.fr
|
||||
Expect:
|
||||
- ''
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: ''
|
||||
headers:
|
||||
Server:
|
||||
- openresty
|
||||
Date:
|
||||
- Mon, 07 Oct 2024 14:41:59 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Content-Length:
|
||||
- '33'
|
||||
X-Ratelimit-Remaining:
|
||||
- '999978'
|
||||
X-Ratelimit-Limit:
|
||||
- '1000000'
|
||||
X-Ratelimit-Reset:
|
||||
- '2024-10-08 00:00:00+00:00'
|
||||
Cache-Control:
|
||||
- no-cache, no-store, max-age=0, must-revalidate
|
||||
Vary:
|
||||
- Accept-Language, Cookie, Host
|
||||
Content-Language:
|
||||
- fr-fr
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Allow-Methods:
|
||||
- POST, GET, OPTIONS
|
||||
Access-Control-Max-Age:
|
||||
- '1000'
|
||||
Access-Control-Allow-Headers:
|
||||
- Authorization, X-Requested-With, Origin, ODS-API-Analytics-App, ODS-API-Analytics-Embed-Type,
|
||||
ODS-API-Analytics-Embed-Referrer, ODS-Widgets-Version, Accept
|
||||
Access-Control-Expose-Headers:
|
||||
- ODS-Explore-API-Deprecation, Link, X-RateLimit-Remaining, X-RateLimit-Limit,
|
||||
X-RateLimit-Reset, X-RateLimit-dataset-Remaining, X-RateLimit-dataset-Limit,
|
||||
X-RateLimit-dataset-Reset
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Permissions-Policy:
|
||||
- midi=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()
|
||||
Content-Security-Policy:
|
||||
- upgrade-insecure-requests;
|
||||
X-Ua-Compatible:
|
||||
- IE=edge
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"total_count": 0, "results": []}'
|
||||
recorded_at: Mon, 07 Oct 2024 14:41:59 GMT
|
||||
recorded_with: VCR 6.2.0
|
100
spec/fixtures/cassettes/annuaire_service_public_success_20004021000060.yml
vendored
Normal file
100
spec/fixtures/cassettes/annuaire_service_public_success_20004021000060.yml
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api-lannuaire.service-public.fr/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=siret:20004021000060
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- demarches.gouv.fr
|
||||
Expect:
|
||||
- ''
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: ''
|
||||
headers:
|
||||
Server:
|
||||
- openresty
|
||||
Date:
|
||||
- Mon, 07 Oct 2024 14:41:57 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Content-Length:
|
||||
- '2573'
|
||||
X-Ratelimit-Remaining:
|
||||
- '999979'
|
||||
X-Ratelimit-Limit:
|
||||
- '1000000'
|
||||
X-Ratelimit-Reset:
|
||||
- '2024-10-08 00:00:00+00:00'
|
||||
Cache-Control:
|
||||
- no-cache, no-store, max-age=0, must-revalidate
|
||||
Vary:
|
||||
- Accept-Language, Cookie, Host
|
||||
Content-Language:
|
||||
- fr-fr
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Allow-Methods:
|
||||
- POST, GET, OPTIONS
|
||||
Access-Control-Max-Age:
|
||||
- '1000'
|
||||
Access-Control-Allow-Headers:
|
||||
- Authorization, X-Requested-With, Origin, ODS-API-Analytics-App, ODS-API-Analytics-Embed-Type,
|
||||
ODS-API-Analytics-Embed-Referrer, ODS-Widgets-Version, Accept
|
||||
Access-Control-Expose-Headers:
|
||||
- ODS-Explore-API-Deprecation, Link, X-RateLimit-Remaining, X-RateLimit-Limit,
|
||||
X-RateLimit-Reset, X-RateLimit-dataset-Remaining, X-RateLimit-dataset-Limit,
|
||||
X-RateLimit-dataset-Reset
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Permissions-Policy:
|
||||
- midi=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()
|
||||
Content-Security-Policy:
|
||||
- upgrade-insecure-requests;
|
||||
X-Ua-Compatible:
|
||||
- IE=edge
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"total_count": 1, "results": [{"plage_ouverture": "[{\"nom_jour_debut\":
|
||||
\"Lundi\", \"nom_jour_fin\": \"Jeudi\", \"valeur_heure_debut_1\": \"08:00:00\",
|
||||
\"valeur_heure_fin_1\": \"12:00:00\", \"valeur_heure_debut_2\": \"13:30:00\",
|
||||
\"valeur_heure_fin_2\": \"17:30:00\", \"commentaire\": \"\"}, {\"nom_jour_debut\":
|
||||
\"Vendredi\", \"nom_jour_fin\": \"Vendredi\", \"valeur_heure_debut_1\": \"08:00:00\",
|
||||
\"valeur_heure_fin_1\": \"12:00:00\", \"valeur_heure_debut_2\": \"\", \"valeur_heure_fin_2\":
|
||||
\"\", \"commentaire\": \"\"}]", "site_internet": "[{\"libelle\": \"\", \"valeur\":
|
||||
\"https://www.cc-lacsgorgesverdon.fr/\"}]", "copyright": "Direction de l''information
|
||||
l\u00e9gale et administrative (Premier ministre)", "siren": "200040210", "ancien_code_pivot":
|
||||
"epci-83007-01", "reseau_social": null, "texte_reference": null, "partenaire":
|
||||
null, "telecopie": null, "nom": "Communaut\u00e9 de communes - Lacs et Gorges
|
||||
du Verdon", "siret": "20004021000060", "itm_identifiant": "999974", "sigle":
|
||||
null, "affectation_personne": null, "date_modification": "31/01/2024 14:25:10",
|
||||
"adresse_courriel": "redacted@email.fr", "service_disponible": null, "organigramme":
|
||||
null, "pivot": "[{\"type_service_local\": \"epci\", \"code_insee_commune\":
|
||||
[\"83007\"]}]", "partenaire_identifiant": null, "ancien_identifiant": null,
|
||||
"id": "3b9ce22d-f7bd-46d9-82d1-8b44c8d08e39", "ancien_nom": null, "commentaire_plage_ouverture":
|
||||
null, "annuaire": null, "tchat": null, "hierarchie": null, "categorie": "SL",
|
||||
"sve": null, "telephone_accessible": null, "application_mobile": null, "version_type":
|
||||
"Publiable", "type_repertoire": null, "telephone": "[{\"valeur\": \"04 94
|
||||
70 00 00\", \"description\": \"\"}]", "version_etat_modification": null, "date_creation":
|
||||
"11/05/2017 11:28:41", "partenaire_date_modification": null, "mission": null,
|
||||
"formulaire_contact": "https://www.cc-lacsgorgesverdon.fr/contacts-comcom",
|
||||
"version_source": null, "type_organisme": null, "code_insee_commune": "83007",
|
||||
"statut_de_diffusion": "true", "adresse": "[{\"type_adresse\": \"Adresse\",
|
||||
\"complement1\": \"\", \"complement2\": \"\", \"numero_voie\": \"242 avenue
|
||||
Albert-1er\", \"service_distribution\": \"\", \"code_postal\": \"83630\",
|
||||
\"nom_commune\": \"Aups\", \"pays\": \"\", \"continent\": \"\", \"longitude\":
|
||||
\"6.22516\", \"latitude\": \"43.627448\", \"accessibilite\": \"ACC\", \"note_accessibilite\":
|
||||
\"ascenseur\"}]", "url_service_public": "https://lannuaire.service-public.fr/provence-alpes-cote-d-azur/var/3b9ce22d-f7bd-46d9-82d1-8b44c8d08e39",
|
||||
"information_complementaire": null, "date_diffusion": null}]}'
|
||||
recorded_at: Mon, 07 Oct 2024 14:41:57 GMT
|
||||
recorded_with: VCR 6.2.0
|
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PrefillableFromServicePublicConcern, type: :model do
|
||||
let(:siret) { '20004021000060' }
|
||||
let(:service) { build(:service, siret:) }
|
||||
|
||||
describe '#prefill_from_siret' do
|
||||
let(:service) { Service.new(siret:) }
|
||||
subject { service.prefill_from_siret }
|
||||
context 'when API call is successful' do
|
||||
it 'prefills service attributes' do
|
||||
VCR.use_cassette('annuaire_service_public_success_20004021000060') do
|
||||
expect(subject).to be_success
|
||||
|
||||
expect(service.nom).to eq("Communauté de communes - Lacs et Gorges du Verdon")
|
||||
expect(service.email).to eq("redacted@email.fr")
|
||||
expect(service.telephone).to eq("04 94 70 00 00")
|
||||
expect(service.horaires).to eq("Lundi au Jeudi : de 8:00 à 12:00 et de 13:30 à 17:30\nVendredi : de 8:00 à 12:00")
|
||||
expect(service.adresse).to eq("242 avenue Albert-1er 83630 Aups")
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not overwrite existing attributes' do
|
||||
service.nom = "Existing Name"
|
||||
service.email = "existing@email.com"
|
||||
|
||||
VCR.use_cassette('annuaire_service_public_success_20004021000060') do
|
||||
service.prefill_from_siret
|
||||
|
||||
expect(service.nom).to eq("Existing Name")
|
||||
expect(service.email).to eq("existing@email.com")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API call do not find siret' do
|
||||
let(:siret) { '20004021000000' }
|
||||
it 'returns a failure result' do
|
||||
VCR.use_cassette('annuaire_service_public_failure_20004021000000') do
|
||||
expect(subject).to be_failure
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#denormalize_plage_ouverture' do
|
||||
it 'correctly formats opening hours with one time range' do
|
||||
data = [
|
||||
{
|
||||
"nom_jour_debut" => "Lundi",
|
||||
"nom_jour_fin" => "Vendredi",
|
||||
"valeur_heure_debut_1" => "09:00:00",
|
||||
"valeur_heure_fin_1" => "17:00:00"
|
||||
}
|
||||
]
|
||||
expect(service.send(:denormalize_plage_ouverture, data)).to eq("Lundi au Vendredi : de 9:00 à 17:00")
|
||||
end
|
||||
|
||||
it 'correctly formats opening hours with two time ranges' do
|
||||
data = [
|
||||
{
|
||||
"nom_jour_debut" => "Lundi",
|
||||
"nom_jour_fin" => "Jeudi",
|
||||
"valeur_heure_debut_1" => "08:00:00",
|
||||
"valeur_heure_fin_1" => "12:00:00",
|
||||
"valeur_heure_debut_2" => "13:30:00",
|
||||
"valeur_heure_fin_2" => "17:30:00"
|
||||
}, {
|
||||
"nom_jour_debut" => "Vendredi",
|
||||
"nom_jour_fin" => "Vendredi",
|
||||
"valeur_heure_debut_1" => "08:00:00",
|
||||
"valeur_heure_fin_1" => "12:00:00"
|
||||
}
|
||||
]
|
||||
expect(service.send(:denormalize_plage_ouverture, data)).to eq("Lundi au Jeudi : de 8:00 à 12:00 et de 13:30 à 17:30\nVendredi : de 8:00 à 12:00")
|
||||
end
|
||||
|
||||
it 'includes comments when present' do
|
||||
data = [
|
||||
{
|
||||
"nom_jour_debut" => "Lundi",
|
||||
"nom_jour_fin" => "Vendredi",
|
||||
"valeur_heure_debut_1" => "09:00:00",
|
||||
"valeur_heure_fin_1" => "17:00:00",
|
||||
"commentaire" => "Fermé les jours fériés"
|
||||
}
|
||||
]
|
||||
expect(service.send(:denormalize_plage_ouverture, data)).to eq("Lundi au Vendredi : de 9:00 à 17:00 (Fermé les jours fériés)")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue