Merge pull request #9648 from demarches-simplifiees/add-rnf-service

Ajout d'un nouveau référentiel : le Répertoire National des Fondations (RNF)
This commit is contained in:
Eric Leroy-Terquem 2023-11-08 09:06:47 +00:00 committed by GitHub
commit 0968f02a26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 378 additions and 14 deletions

View file

@ -48,6 +48,8 @@
= render partial: "shared/champs/regions/show", locals: { champ: champ } = render partial: "shared/champs/regions/show", locals: { champ: champ }
- when TypeDeChamp.type_champs.fetch(:rna) - when TypeDeChamp.type_champs.fetch(:rna)
= render partial: "shared/champs/rna/show", locals: { champ: champ, profile: @profile } = render partial: "shared/champs/rna/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:rnf)
= render partial: "shared/champs/rnf/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:epci) - when TypeDeChamp.type_champs.fetch(:epci)
= render partial: "shared/champs/epci/show", locals: { champ: champ } = render partial: "shared/champs/epci/show", locals: { champ: champ }
- when TypeDeChamp.type_champs.fetch(:cojo) - when TypeDeChamp.type_champs.fetch(:cojo)
@ -60,4 +62,3 @@
%p= helpers.number_with_html_delimiter(champ.to_s) %p= helpers.number_with_html_delimiter(champ.to_s)
- else - else
= helpers.format_text_value(champ.to_s.strip) # format already wrap in p = helpers.format_text_value(champ.to_s.strip) # format already wrap in p

View file

@ -0,0 +1,2 @@
class EditableChamp::RNFComponent < EditableChamp::EditableChampBaseComponent
end

View file

@ -0,0 +1,5 @@
---
en:
rnf_info_error: No foundation found
rnf_info_pending: RNF verification pending
rnf_info_success: "This RNF matches %{title}"

View file

@ -0,0 +1,5 @@
---
fr:
rnf_info_error: Aucune fondation trouvée
rnf_info_pending: Vérification du RNF en cours
rnf_info_success: "Ce RNF correspond à %{title}"

View file

@ -0,0 +1,9 @@
= @form.text_field :external_id, required: @champ.required?, class: "width-33-desktop fr-input small-margin", id: @champ.input_id
.rnf-info{ id: dom_id(@champ, :rnf_info) }
- if @champ.fetch_external_data_error?
%p.fr-error-text= t('.rnf_info_error')
- elsif @champ.fetch_external_data_pending?
%p.fr-info-text= t('.rnf_info_pending')
- elsif @champ.data?
%p.fr-info-text= t('.rnf_info_success', title: @champ.title)

View file

@ -110,6 +110,7 @@ class API::V2::Schema < GraphQL::Schema
Types::Champs::Descriptor::RegionChampDescriptorType, Types::Champs::Descriptor::RegionChampDescriptorType,
Types::Champs::Descriptor::RepetitionChampDescriptorType, Types::Champs::Descriptor::RepetitionChampDescriptorType,
Types::Champs::Descriptor::RNAChampDescriptorType, Types::Champs::Descriptor::RNAChampDescriptorType,
Types::Champs::Descriptor::RNFChampDescriptorType,
Types::Champs::Descriptor::SiretChampDescriptorType, Types::Champs::Descriptor::SiretChampDescriptorType,
Types::Champs::Descriptor::TextareaChampDescriptorType, Types::Champs::Descriptor::TextareaChampDescriptorType,
Types::Champs::Descriptor::TextChampDescriptorType, Types::Champs::Descriptor::TextChampDescriptorType,

View file

@ -3553,6 +3553,34 @@ type RNAChampDescriptor implements ChampDescriptor {
type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.") type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.")
} }
type RNFChampDescriptor implements ChampDescriptor {
"""
Description des champs dun bloc répétable.
"""
champDescriptors: [ChampDescriptor!] @deprecated(reason: "Utilisez le champ `RepetitionChampDescriptor.champ_descriptors` à la place.")
"""
Description du champ.
"""
description: String
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
Est-ce que le champ est obligatoire ?
"""
required: Boolean!
"""
Type de la valeur du champ.
"""
type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.")
}
type Region { type Region {
code: String! code: String!
name: String! name: String!
@ -4072,10 +4100,15 @@ enum TypeDeChamp {
repetition repetition
""" """
RNA RNA (Répertoire national des associations)
""" """
rna rna
"""
RNF (Répertoire national des fondations)
"""
rnf
""" """
Numéro Siret Numéro Siret
""" """

View file

@ -72,6 +72,8 @@ module Types
Types::Champs::Descriptor::PieceJustificativeChampDescriptorType Types::Champs::Descriptor::PieceJustificativeChampDescriptorType
when TypeDeChamp.type_champs.fetch(:rna) when TypeDeChamp.type_champs.fetch(:rna)
Types::Champs::Descriptor::RNAChampDescriptorType Types::Champs::Descriptor::RNAChampDescriptorType
when TypeDeChamp.type_champs.fetch(:rnf)
Types::Champs::Descriptor::RNFChampDescriptorType
when TypeDeChamp.type_champs.fetch(:carte) when TypeDeChamp.type_champs.fetch(:carte)
Types::Champs::Descriptor::CarteChampDescriptorType Types::Champs::Descriptor::CarteChampDescriptorType
when TypeDeChamp.type_champs.fetch(:repetition) when TypeDeChamp.type_champs.fetch(:repetition)

View file

@ -0,0 +1,5 @@
module Types::Champs::Descriptor
class RNFChampDescriptorType < Types::BaseObject
implements Types::ChampDescriptorType
end
end

View file

@ -205,9 +205,7 @@ class Champ < ApplicationRecord
end end
def log_fetch_external_data_exception(exception) def log_fetch_external_data_exception(exception)
exceptions = self.fetch_external_data_exceptions ||= [] update_column(:fetch_external_data_exceptions, [exception.inspect])
exceptions << exception.inspect
update_column(:fetch_external_data_exceptions, exceptions)
end end
def fetch_external_data? def fetch_external_data?
@ -218,10 +216,12 @@ class Champ < ApplicationRecord
false false
end end
def fetch_external_data_error?
fetch_external_data_exceptions.present? && self.external_id.present?
end
def fetch_external_data_pending? def fetch_external_data_pending?
# We don't have a good mechanism right now to know if the last fetch has errored. So, in order fetch_external_data? && poll_external_data? && external_id.present? && data.nil? && !fetch_external_data_error?
# to ensure we don't poll to infinity, we stop after 5 minutes no matter what.
fetch_external_data? && poll_external_data? && external_id.present? && data.nil? && updated_at > 5.minutes.ago
end end
def fetch_external_data def fetch_external_data
@ -289,6 +289,7 @@ class Champ < ApplicationRecord
def fetch_external_data_later def fetch_external_data_later
if fetch_external_data? && external_id.present? && data.nil? if fetch_external_data? && external_id.present? && data.nil?
update_column(:fetch_external_data_exceptions, [])
ChampFetchExternalDataJob.perform_later(self, external_id) ChampFetchExternalDataJob.perform_later(self, external_id)
end end
end end

View file

@ -0,0 +1,23 @@
class Champs::RNFChamp < Champ
store_accessor :data, :title, :email, :phone, :createdAt, :updatedAt, :dissolvedAt, :address, :status
def rnf_id
external_id
end
def fetch_external_data
RNFService.new.(rnf_id:)
end
def fetch_external_data?
true
end
def poll_external_data?
true
end
def blank?
rnf_id.blank?
end
end

View file

@ -51,6 +51,7 @@ class TypeDeChamp < ApplicationRecord
yes_no: CHOICE, yes_no: CHOICE,
annuaire_education: REFERENTIEL_EXTERNE, annuaire_education: REFERENTIEL_EXTERNE,
rna: REFERENTIEL_EXTERNE, rna: REFERENTIEL_EXTERNE,
rnf: REFERENTIEL_EXTERNE,
carte: REFERENTIEL_EXTERNE, carte: REFERENTIEL_EXTERNE,
cnaf: REFERENTIEL_EXTERNE, cnaf: REFERENTIEL_EXTERNE,
dgfip: REFERENTIEL_EXTERNE, dgfip: REFERENTIEL_EXTERNE,
@ -91,6 +92,7 @@ class TypeDeChamp < ApplicationRecord
yes_no: 'yes_no', yes_no: 'yes_no',
annuaire_education: 'annuaire_education', annuaire_education: 'annuaire_education',
rna: 'rna', rna: 'rna',
rnf: 'rnf',
carte: 'carte', carte: 'carte',
cnaf: 'cnaf', cnaf: 'cnaf',
dgfip: 'dgfip', dgfip: 'dgfip',

View file

@ -0,0 +1,2 @@
class TypesDeChamp::RNFTypeDeChamp < TypesDeChamp::TextTypeDeChamp
end

36
app/schemas/rnf.json Normal file
View file

@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://demarches-simplifiees.fr/rnf.schema.json",
"title": "RNF",
"type": "object",
"properties": {
"id": { "type": "integer" },
"rnfId": { "type": "string" },
"type": { "type": "string" },
"department": { "type": "string" },
"title": { "type": "string" },
"dissolvedAt": { "type": ["string", "null"] },
"phone": { "type": "string" },
"email": { "type": "string" },
"addressId": { "type": "integer" },
"address": {
"id": { "type": "integer" },
"createdAt":{ "type": "string" },
"updatedAt":{ "type": "string" },
"label":{ "type": "string" },
"type":{ "type": "string" },
"streetAddress":{ "type": "string" },
"streetNumber":{ "type": "string" },
"streetName":{ "type": "string" },
"postalCode":{ "type": "string" },
"cityName":{ "type": "string" },
"cityCode":{ "type": "string" },
"departmentName":{ "type": "string" },
"departmentCode":{ "type": "string" },
"regionName":{ "type": "string" },
"regionCode":{ "type": "string" }
},
"status": { "type": ["string", "null"] },
"persons": { "type": "array" }
}
}

View file

@ -0,0 +1,25 @@
class RNFService
include Dry::Monads[:result]
def call(rnf_id:)
result = API::Client.new.(url: "#{url}/#{rnf_id}", schema:)
case result
in Success(body:)
Success(body)
in Failure(code:, reason:) if code.in?(401..403)
Failure(API::Client::Error[:unauthorized, code, false, reason])
else
result
end
end
private
def schema
JSONSchemer.schema(Rails.root.join('app/schemas/rnf.json'))
end
def url
"#{API_RNF_URL}/api/foundations"
end
end

View file

@ -0,0 +1,30 @@
- if champ.data.blank?
%p= t('.not_found', rnf: champ.rnf_id)
- else
.fr-background-alt--grey.fr-p-3v
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.rnf_id")) do |c|
- c.with_value do
%p
= champ.rnf_id
= render Dsfr::CopyButtonComponent.new(text: champ.rnf_id, title: t("activemodel.attributes.rnf_champ.paste"), success: t("activemodel.attributes.rnf_champ.paste_success"))
- ['title', 'email', 'phone','status'].each do |scope|
- if champ.data[scope].present?
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.#{scope}")) do |c|
- c.with_value do
%p= champ.data[scope]
- ['createdAt', 'updatedAt', 'dissolvedAt'].each do |scope|
- if champ.data[scope].present?
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.#{scope}")) do |c|
- c.with_value do
%p= l(champ.data[scope].to_date)
- if champ.data['address'].present?
- ['label', 'cityCode', 'postalCode'].each do |scope|
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.#{scope}")) do |c|
- c.with_value do
%p= champ.data['address'][scope]
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.department")) do |c|
- c.with_value do
%p= "#{champ.data['address']['departmentCode']} #{champ.data['address']['departmentName']}"

View file

@ -7,6 +7,7 @@ API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1") API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1")
API_COJO_URL = ENV.fetch("API_COJO_URL", nil) API_COJO_URL = ENV.fetch("API_COJO_URL", nil)
API_RNF_URL = ENV.fetch("API_RNF_URL", "https://rnf.dso.numerique-interieur.com")
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2") HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2") SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3") SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3")

View file

@ -14,6 +14,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'IP' inflect.acronym 'IP'
inflect.acronym 'JSON' inflect.acronym 'JSON'
inflect.acronym 'RNA' inflect.acronym 'RNA'
inflect.acronym 'RNF'
inflect.acronym 'URL' inflect.acronym 'URL'
inflect.acronym 'SVA' inflect.acronym 'SVA'
inflect.acronym 'SVR' inflect.acronym 'SVR'

View file

@ -0,0 +1,24 @@
en:
activemodel:
attributes:
rnf_champ:
rnf_id: RNF id
data:
title: Foundation name
email: Email
phone: Phone
createdAt: Created at
updatedAt: Updated at
dissolvedAt: Dissolved at
address: Address
status: Status
cityCode: City code
postalCode: Postal code
department: Department
paste: Copy the RNF to the clipboard
paste_success: The RNF has been copied to the clipboard
activerecord:
attributes:
champs/rnf_champ:
hints:
value: "Expected format : 075-FDD-00003-01"

View file

@ -0,0 +1,24 @@
fr:
activemodel:
attributes:
rnf_champ:
rnf_id: Numéro RNF
data:
title: Nom de la fondation
email: Email
phone: Téléphone
createdAt: Créée le
updatedAt: Mise à jour le
dissolvedAt: Dissoute le
address: Adresse
status: Statut
cityCode: Code INSEE
postalCode: Code postal
department: Département
paste: Copier le RNF dans le presse-papier
paste_success: Le RNF a été copié dans le presse-papier
activerecord:
attributes:
champs/rnf_champ:
hints:
value: "Format attendu : 075-FDD-00003-01"

View file

@ -48,6 +48,7 @@ en:
yes_no_false: 'no' yes_no_false: 'no'
annuaire_education: 'Schooling directory' annuaire_education: 'Schooling directory'
rna: 'RNA' rna: 'RNA'
rnf: 'RNF'
carte: 'Card' carte: 'Card'
cnaf: 'Data from Caisse nationale des allocations familiales' cnaf: 'Data from Caisse nationale des allocations familiales'
dgfip: 'Data from Direction générale des Finances publiques' dgfip: 'Data from Direction générale des Finances publiques'

View file

@ -47,7 +47,8 @@ fr:
yes_no_true: 'oui' yes_no_true: 'oui'
yes_no_false: 'non' yes_no_false: 'non'
annuaire_education: 'Annuaire de léducation' annuaire_education: 'Annuaire de léducation'
rna: 'RNA' rna: 'RNA (Répertoire national des associations)'
rnf: 'RNF (Répertoire national des fondations)'
carte: 'Carte' carte: 'Carte'
cnaf: 'Données de la Caisse nationale des allocations familiales' cnaf: 'Données de la Caisse nationale des allocations familiales'
dgfip: 'Données de la Direction générale des Finances publiques' dgfip: 'Données de la Direction générale des Finances publiques'

View file

@ -30,6 +30,9 @@ en:
data_fetched: "This RNA number is linked to %{title}" data_fetched: "This RNA number is linked to %{title}"
not_found: "No association found" not_found: "No association found"
network_error: "A network error has prevented the association associated with this RNA to be fetched" network_error: "A network error has prevented the association associated with this RNA to be fetched"
rnf:
show:
not_found: "RNF %{rnf} (no foundation found)"
dgfip: dgfip:
show: show:
not_filled: not filled not_filled: not filled

View file

@ -32,6 +32,9 @@ fr:
data_fetched: "Ce RNA correspond à %{title}" data_fetched: "Ce RNA correspond à %{title}"
not_found: "Aucun établissement trouvé" not_found: "Aucun établissement trouvé"
network_error: "Une erreur réseau a empêché lassociation liée à ce RNA dêtre trouvée" network_error: "Une erreur réseau a empêché lassociation liée à ce RNA dêtre trouvée"
rnf:
show:
not_found: "RNF %{rnf} (aucune fondation trouvée)"
dgfip: dgfip:
show: show:
not_filled: non renseigné not_filled: non renseigné

View file

@ -242,6 +242,10 @@ FactoryBot.define do
type_de_champ { association :type_de_champ_cojo, procedure: dossier.procedure } type_de_champ { association :type_de_champ_cojo, procedure: dossier.procedure }
end end
factory :champ_rnf, class: 'Champs::RNFChamp' do
type_de_champ { association :type_de_champ_rnf, procedure: dossier.procedure }
end
factory :champ_expression_reguliere, class: 'Champs::ExpressionReguliereChamp' do factory :champ_expression_reguliere, class: 'Champs::ExpressionReguliereChamp' do
type_de_champ { association :type_de_champ_expression_reguliere, procedure: dossier.procedure } type_de_champ { association :type_de_champ_expression_reguliere, procedure: dossier.procedure }
end end

View file

@ -187,6 +187,9 @@ FactoryBot.define do
factory :type_de_champ_cojo do factory :type_de_champ_cojo do
type_champ { TypeDeChamp.type_champs.fetch(:cojo) } type_champ { TypeDeChamp.type_champs.fetch(:cojo) }
end end
factory :type_de_champ_rnf do
type_champ { TypeDeChamp.type_champs.fetch(:rnf) }
end
factory :type_de_champ_repetition do factory :type_de_champ_repetition do
type_champ { TypeDeChamp.type_champs.fetch(:repetition) } type_champ { TypeDeChamp.type_champs.fetch(:repetition) }

View file

@ -0,0 +1,6 @@
{
"id":3,
"createdAt":"2023-09-07T13:26:10.358Z",
"updatedAt":"2023-09-07T13:26:10.358Z",
"rnfId": 750000301
}

32
spec/fixtures/files/api_rnf/valid.json vendored Normal file
View file

@ -0,0 +1,32 @@
{
"id":3,
"createdAt":"2023-09-07T13:26:10.358Z",
"updatedAt":"2023-09-07T13:26:10.358Z",
"rnfId":"075-FDD-00003-01",
"type":"FDD",
"department":"75",
"title":"Fondation SFR",
"dissolvedAt":null,
"phone":"+33185060000",
"email":"fondation@sfr.fr",
"addressId":3,
"address": {
"id":3,
"createdAt":"2023-09-07T13:26:10.358Z",
"updatedAt":"2023-09-07T13:26:10.358Z",
"label":"16 Rue du Général de Boissieu 75015 Paris",
"type":"housenumber",
"streetAddress":"16 Rue du Général de Boissieu",
"streetNumber":"16",
"streetName":"Rue du Général de Boissieu",
"postalCode":"75015",
"cityName":"Paris",
"cityCode":"75115",
"departmentName":"Paris",
"departmentCode":"75",
"regionName":"Île-de-France",
"regionCode":"11"
},
"status":null,
"persons":[]
}

View file

@ -14,9 +14,9 @@ describe '20220705164551_remove_unused_champs' do
describe 'remove_unused_champs' do describe 'remove_unused_champs' do
it "with bad champs" do it "with bad champs" do
expect(Champ.where(dossier: dossier).count).to eq(42) expect(Champ.where(dossier: dossier).count).to eq(43)
run_task run_task
expect(Champ.where(dossier: dossier).count).to eq(41) expect(Champ.where(dossier: dossier).count).to eq(42)
end end
end end
end end

View file

@ -0,0 +1,76 @@
describe Champs::RNFChamp, type: :model do
let(:champ) { build(:champ_rnf, external_id:) }
let(:stub) { stub_request(:get, "#{url}/#{external_id}").to_return(body:, status:) }
let(:url) { RNFService.new.send(:url) }
let(:body) { Rails.root.join('spec', 'fixtures', 'files', 'api_rnf', "#{response_type}.json").read }
let(:external_id) { '075-FDD-00003-01' }
let(:status) { 200 }
let(:response_type) { 'valid' }
describe 'fetch_external_data' do
subject { stub; champ.fetch_external_data }
context 'success' do
it do
expect(subject.value!).to eq({
id: 3,
rnfId: '075-FDD-00003-01',
type: 'FDD',
department: '75',
title: 'Fondation SFR',
dissolvedAt: nil,
phone: '+33185060000',
email: 'fondation@sfr.fr',
addressId: 3,
createdAt: "2023-09-07T13:26:10.358Z",
updatedAt: "2023-09-07T13:26:10.358Z",
address: {
id: 3,
createdAt: "2023-09-07T13:26:10.358Z",
updatedAt: "2023-09-07T13:26:10.358Z",
label: "16 Rue du Général de Boissieu 75015 Paris",
type: "housenumber",
streetAddress: "16 Rue du Général de Boissieu",
streetNumber: "16",
streetName: "Rue du Général de Boissieu",
postalCode: "75015",
cityName: "Paris",
cityCode: "75115",
departmentName: "Paris",
departmentCode: "75",
regionName: "Île-de-France",
regionCode: "11"
},
status: nil,
persons: []
})
end
end
context 'failure (schema)' do
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_falsey
expect(subject.failure.reason).to be_a(API::Client::SchemaError)
}
end
context 'failure (http 500)' do
let(:status) { 500 }
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_truthy
expect(subject.failure.reason).to be_a(API::Client::HTTPError)
}
end
context 'failure (http 401)' do
let(:status) { 401 }
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_falsey
expect(subject.failure.reason).to be_a(API::Client::HTTPError)
}
end
end
end

View file

@ -90,7 +90,8 @@ describe ProcedureExportService do
"epci (Code)", "epci (Code)",
"epci (Département)", "epci (Département)",
"cojo", "cojo",
"expression_reguliere" "expression_reguliere",
"rnf"
] ]
end end
@ -202,7 +203,8 @@ describe ProcedureExportService do
"epci (Code)", "epci (Code)",
"epci (Département)", "epci (Département)",
"cojo", "cojo",
"expression_reguliere" "expression_reguliere",
"rnf"
] ]
end end
@ -297,7 +299,8 @@ describe ProcedureExportService do
"epci (Code)", "epci (Code)",
"epci (Département)", "epci (Département)",
"cojo", "cojo",
"expression_reguliere" "expression_reguliere",
"rnf"
] ]
end end