From 098b5cb9416b1f78e56f2ae6a60cceb24645c469 Mon Sep 17 00:00:00 2001 From: Thomas COMES Date: Tue, 28 Jul 2026 15:44:50 +0200 Subject: [PATCH 1/5] Request HubEE tokens with the datapass scope instead of ADMIN Subscription provisioning only needs to create subscriptions; admin rights were granted solely to allow auto-activation, which is going away. --- site/app/clients/hubee_api_authentication.rb | 2 +- .../clients/hubee_api_authentication_spec.rb | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 site/spec/clients/hubee_api_authentication_spec.rb diff --git a/site/app/clients/hubee_api_authentication.rb b/site/app/clients/hubee_api_authentication.rb index e7e0a68195..f1778c9267 100644 --- a/site/app/clients/hubee_api_authentication.rb +++ b/site/app/clients/hubee_api_authentication.rb @@ -3,7 +3,7 @@ class HubEEAPIAuthentication < AbstractHubEEAPIClient def access_token http_connection.post( auth_url, - 'grant_type=client_credentials&scope=ADMIN', + 'grant_type=client_credentials&scope=DATAPASS', { 'Authorization' => "Basic #{encoded_client_id_and_secret}" } diff --git a/site/spec/clients/hubee_api_authentication_spec.rb b/site/spec/clients/hubee_api_authentication_spec.rb new file mode 100644 index 0000000000..b10bbecfab --- /dev/null +++ b/site/spec/clients/hubee_api_authentication_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe HubEEAPIAuthentication do + describe '#access_token' do + subject(:access_token) { described_class.new.access_token } + + let(:auth_url) { Rails.application.credentials.hubee_auth_url } + + before do + stub_request(:post, auth_url) + .with(body: 'grant_type=client_credentials&scope=datapass') + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { access_token: 'hubee_access_token' }.to_json + ) + end + + it 'requests a token with the datapass scope' do + expect(access_token).to eq('hubee_access_token') + end + end +end From 5a9f140dbf7daa7ec62ab619bbc6bb53a113c4c7 Mon Sep 17 00:00:00 2001 From: Thomas COMES Date: Tue, 28 Jul 2026 15:45:54 +0200 Subject: [PATCH 2/5] Stop auto-activating HubEE subscriptions Activation becomes a manual step for the services instructeurs, like every other OSL; FQF volume is low enough that automation isn't worth holding admin-level credentials. --- site/app/clients/hubee_api_client.rb | 28 +---------- site/spec/clients/hubee_api_client_spec.rb | 57 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/site/app/clients/hubee_api_client.rb b/site/app/clients/hubee_api_client.rb index a4a153d262..6d64ba9d6d 100644 --- a/site/app/clients/hubee_api_client.rb +++ b/site/app/clients/hubee_api_client.rb @@ -37,10 +37,8 @@ def create_organization(organization, email) raise end - def create_subscription(authorization_request, organization_payload, process_code, editor_payload = {}) - subscription_payload = find_or_create_inactive_subscription(authorization_request, organization_payload, process_code) - activate_subscription(subscription_payload, editor_payload) - subscription_payload + def create_subscription(authorization_request, organization_payload, process_code, _editor_payload = {}) + find_or_create_inactive_subscription(authorization_request, organization_payload, process_code) end def find_subscription(_authorization_request, organization_payload, process_code) @@ -73,28 +71,6 @@ def http_connection(&block) private - def activate_subscription(subscription_payload, editor_payload = {}) # rubocop:disable Metrics/AbcSize - subscription_id = Hash(subscription_payload)['id'] - return if subscription_id.blank? - - payload = subscription_payload.with_indifferent_access.merge({ - status: 'Actif', - activateDateTime: DateTime.now.iso8601, - accessMode: 'API', - notificationFrequency: 'Aucune' - }.with_indifferent_access) - - payload.delete('id') - payload.delete('creationDateTime') - payload.merge!(editor_payload.with_indifferent_access) - - http_connection.put( - "#{host}/referential/v1/subscriptions/#{subscription_id}", - payload.to_json, - 'Content-Type' => 'application/json' - ).body - end - def create_inactive_subscription(authorization_request, organization_payload, process_code) # rubocop:disable Metrics/AbcSize http_connection.post( "#{host}/referential/v1/subscriptions", diff --git a/site/spec/clients/hubee_api_client_spec.rb b/site/spec/clients/hubee_api_client_spec.rb index 87012a0303..d5b4a4c18e 100644 --- a/site/spec/clients/hubee_api_client_spec.rb +++ b/site/spec/clients/hubee_api_client_spec.rb @@ -64,4 +64,61 @@ end end end + + describe '#create_subscription' do + subject(:create_subscription) { described_class.new.create_subscription(authorization_request, organization_payload, process_code) } + + let(:authorization_request) { create(:authorization_request, :with_demandeur, api: 'particulier') } + let(:organization_payload) { hubee_organization_payload } + let(:process_code) { 'FormulaireQF' } + let(:host) { Rails.application.credentials.hubee_api_url } + let(:subscription_payload) { hubee_subscription_payload(authorization_request:, process_code:) } + + context 'when the subscription does not exist yet' do + before do + stub_request(:post, "#{host}/referential/v1/subscriptions").to_return( + status: 201, + headers: { 'Content-Type' => 'application/json' }, + body: subscription_payload.to_json + ) + end + + it 'returns the created subscription payload' do + expect(create_subscription).to eq(subscription_payload) + end + + it 'does not activate the subscription' do + create_subscription + + expect(a_request(:put, %r{#{host}/referential/v1/subscriptions/})).not_to have_been_made + end + end + + context 'when the subscription already exists' do + before do + stub_request(:post, "#{host}/referential/v1/subscriptions").to_return( + status: 400, + headers: { 'Content-Type' => 'application/json' }, + body: { 'errors' => [{ 'message' => 'Subscription already exists' }] }.to_json + ) + stub_request(:get, "#{host}/referential/v1/subscriptions") + .with(query: { companyRegister: organization_payload['companyRegister'], processCode: process_code }) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: [subscription_payload].to_json + ) + end + + it 'returns the existing subscription payload' do + expect(create_subscription).to eq(subscription_payload) + end + + it 'does not activate the subscription' do + create_subscription + + expect(a_request(:put, %r{#{host}/referential/v1/subscriptions/})).not_to have_been_made + end + end + end end From ced4b1e6a1b3256c7639f5e5f56872852640b405 Mon Sep 17 00:00:00 2001 From: Thomas COMES Date: Tue, 28 Jul 2026 15:47:28 +0200 Subject: [PATCH 3/5] Drop the editor delegation payload from HubEE subscriptions delegationActor was only sent through the activation request; with activation now manual, the instructeur configures the delegation in HubEE and the INSEE lookup for the editor organization is dead weight. --- site/app/clients/hubee_api_client.rb | 2 +- .../create_hubee_subscription.rb | 34 +-------------- .../create_hubee_subscription_spec.rb | 43 ++++--------------- .../create_formulaire_qf_resources_spec.rb | 10 ----- 4 files changed, 10 insertions(+), 79 deletions(-) diff --git a/site/app/clients/hubee_api_client.rb b/site/app/clients/hubee_api_client.rb index 6d64ba9d6d..a516612bbb 100644 --- a/site/app/clients/hubee_api_client.rb +++ b/site/app/clients/hubee_api_client.rb @@ -37,7 +37,7 @@ def create_organization(organization, email) raise end - def create_subscription(authorization_request, organization_payload, process_code, _editor_payload = {}) + def create_subscription(authorization_request, organization_payload, process_code) find_or_create_inactive_subscription(authorization_request, organization_payload, process_code) end diff --git a/site/app/interactors/datapass_webhook/api_particulier/create_hubee_subscription.rb b/site/app/interactors/datapass_webhook/api_particulier/create_hubee_subscription.rb index 6bf53905c9..d3aadee03e 100644 --- a/site/app/interactors/datapass_webhook/api_particulier/create_hubee_subscription.rb +++ b/site/app/interactors/datapass_webhook/api_particulier/create_hubee_subscription.rb @@ -9,35 +9,7 @@ def call private def create_subscription_on_hubee - hubee_api_client.create_subscription(authorization_request, hubee_organization_payload, process_code, editor_payload) - end - - def editor_organization - @editor_organization ||= begin - organization = Organization.find_or_create_by(siret: service_provider['siret']) - - UpdateOrganizationINSEEPayloadJob.new.perform(organization.id) - organization.reload - - organization - end - end - - def editor_payload - return {} unless editor_subscription? - - { - delegationActor: { - branchCode: editor_organization.code_commune_etablissement, - companyRegister: editor_organization.siret, - type: 'EDT' - }, - accessMode: 'API' - } - end - - def editor_subscription? - service_provider['type'] == 'editor' + hubee_api_client.create_subscription(authorization_request, hubee_organization_payload, process_code) end def hubee_api_client @@ -52,8 +24,4 @@ def save_hubee_subscription_id_to_authorization_request authorization_request.extra_infos['hubee_subscription_id'] = hubee_subscription_payload['id'] authorization_request.save! end - - def service_provider - @service_provider ||= Hash(authorization_request.extra_infos['service_provider']) - end end diff --git a/site/spec/interactors/datapass_webhook/api_particulier/create_hubee_subscription_spec.rb b/site/spec/interactors/datapass_webhook/api_particulier/create_hubee_subscription_spec.rb index 7b21a870f7..6d26900df2 100644 --- a/site/spec/interactors/datapass_webhook/api_particulier/create_hubee_subscription_spec.rb +++ b/site/spec/interactors/datapass_webhook/api_particulier/create_hubee_subscription_spec.rb @@ -15,43 +15,16 @@ allow(hubee_api_client).to receive(:create_subscription).and_return(stripped_hubee_subscription_payload) end - context 'when the authorization request has a service provider' do - let(:authorization_request) { create(:authorization_request, extra_infos: { 'service_provider' => service_provider }) } - let(:editor_siret) { '13002526500013' } - let(:service_provider) { { 'type' => 'editor', 'siret' => editor_siret } } - - context 'when the service provider is an editor' do - let(:insee_api_authentication) { instance_double(INSEEAPIAuthentication, access_token: 'access_token') } - let(:insee_payload) { insee_sirene_api_etablissement_valid_payload(siret: editor_siret, full: true) } - - before do - allow(INSEEAPIAuthentication).to receive(:new).and_return(insee_api_authentication) - stub_request(:get, "https://api.insee.fr/api-sirene/prive/3.11/siret/#{editor_siret}").to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: insee_payload.to_json - ) - end - - it 'creates a subscription on HubEE with the editor payload' do - expect(hubee_api_client).to receive(:create_subscription).with(authorization_request, stripped_hubee_organization_payload, 'FormulaireQF', { delegationActor: { branchCode: '75107', companyRegister: '13002526500013', type: 'EDT' }, accessMode: 'API' }) - interactor - end - end - - context 'when the service provider is not an editor' do - let(:service_provider) { { 'type' => 'service', 'siret' => '123456' } } - - it 'creates a subscription on HubEE without the editor payload' do - expect(hubee_api_client).to receive(:create_subscription).with(authorization_request, stripped_hubee_organization_payload, 'FormulaireQF', {}) - interactor - end - end + it 'creates a subscription on HubEE' do + expect(hubee_api_client).to receive(:create_subscription).with(authorization_request, stripped_hubee_organization_payload, 'FormulaireQF') + interactor end - context 'when the authorization request does not have a service provider' do - it 'creates a subscription on HubEE' do - expect(hubee_api_client).to receive(:create_subscription).with(authorization_request, stripped_hubee_organization_payload, 'FormulaireQF', {}) + context 'when the service provider is an editor' do + let(:authorization_request) { create(:authorization_request, extra_infos: { 'service_provider' => { 'type' => 'editor', 'siret' => '13002526500013' } }) } + + it 'creates the subscription without any editor delegation' do + expect(hubee_api_client).to receive(:create_subscription).with(authorization_request, stripped_hubee_organization_payload, 'FormulaireQF') interactor end end diff --git a/site/spec/organizers/datapass_webhook/create_formulaire_qf_resources_spec.rb b/site/spec/organizers/datapass_webhook/create_formulaire_qf_resources_spec.rb index 99669ea988..2a015f7ab6 100644 --- a/site/spec/organizers/datapass_webhook/create_formulaire_qf_resources_spec.rb +++ b/site/spec/organizers/datapass_webhook/create_formulaire_qf_resources_spec.rb @@ -4,12 +4,8 @@ subject(:interactor) { described_class.call(authorization_request:) } let(:hubee_api_client) { instance_double(HubEEAPIClient) } - let(:insee_api_authentication) { instance_double(INSEEAPIAuthentication, access_token: 'access_token') } - let(:insee_payload) { insee_sirene_api_etablissement_valid_payload(siret: editor_siret, full: true) } let(:authorization_request) { create(:authorization_request, :with_demandeur, api: 'particulier') } let(:subscription_payload) { hubee_subscription_payload(authorization_request:) } - let(:editor_siret) { '13002526500013' } - let(:service_provider) { { 'type' => 'editor', 'siret' => editor_siret } } let(:formulaire_qf_api_client) { instance_double(FormulaireQFAPIClient) } let(:siret) { '12345678901234' } let(:code_commune) { '12345' } @@ -19,12 +15,6 @@ before do allow(HubEEAPIClient).to receive(:new).and_return(hubee_api_client) allow(hubee_api_client).to receive_messages(find_or_create_organization: organization_payload, create_subscription: subscription_payload) - allow(INSEEAPIAuthentication).to receive(:new).and_return(insee_api_authentication) - stub_request(:get, "https://api.insee.fr/entreprises/sirene/V3.11/siret/#{editor_siret}").to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: insee_payload.to_json - ) allow(FormulaireQFAPIClient).to receive(:new).and_return(formulaire_qf_api_client) allow(formulaire_qf_api_client).to receive(:create_collectivity) end From 7bc729a53c7e2508ad317f0dfeaceb785318bc0a Mon Sep 17 00:00:00 2001 From: Thomas COMES Date: Tue, 28 Jul 2026 15:51:52 +0200 Subject: [PATCH 4/5] Collapse the HubEE subscription path after activation removal find_or_create_inactive_subscription was only a seam for bolting activation on top; the shared fixtures also claimed subscriptions come back Actif, a state this app can no longer produce. --- site/app/clients/hubee_api_client.rb | 10 +++----- site/spec/support/hubee_api_mocks.rb | 4 ++-- site/spec/support/insee_sirene_api_mocks.rb | 26 +++++++-------------- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/site/app/clients/hubee_api_client.rb b/site/app/clients/hubee_api_client.rb index a516612bbb..4f389a8d4e 100644 --- a/site/app/clients/hubee_api_client.rb +++ b/site/app/clients/hubee_api_client.rb @@ -38,7 +38,9 @@ def create_organization(organization, email) end def create_subscription(authorization_request, organization_payload, process_code) - find_or_create_inactive_subscription(authorization_request, organization_payload, process_code) + create_inactive_subscription(authorization_request, organization_payload, process_code) + rescue AlreadyExists + find_subscription(authorization_request, organization_payload, process_code) end def find_subscription(_authorization_request, organization_payload, process_code) @@ -100,10 +102,4 @@ def create_inactive_subscription(authorization_request, organization_payload, pr raise end - - def find_or_create_inactive_subscription(authorization_request, organization_payload, process_code) - create_inactive_subscription(authorization_request, organization_payload, process_code) - rescue HubEEAPIClient::AlreadyExists - find_subscription(authorization_request, organization_payload, process_code) - end end diff --git a/site/spec/support/hubee_api_mocks.rb b/site/spec/support/hubee_api_mocks.rb index 45b4de899b..787ca358e1 100644 --- a/site/spec/support/hubee_api_mocks.rb +++ b/site/spec/support/hubee_api_mocks.rb @@ -21,7 +21,7 @@ def hubee_subscription_payload(authorization_request:, organization_payload: hub { 'id' => SecureRandom.uuid, 'datapassId' => authorization_request.external_id.to_i, - 'notificationFrequency' => 'unitaire', + 'notificationFrequency' => 'Aucune', 'processCode' => process_code, 'email' => authorization_request.demandeur.email, 'localAdministrator' => { @@ -29,7 +29,7 @@ def hubee_subscription_payload(authorization_request:, organization_payload: hub 'firstName' => authorization_request.demandeur.first_name, 'lastName' => authorization_request.demandeur.last_name }, - 'status' => 'Actif', + 'status' => 'Inactif', 'subscriber' => { 'branchCode' => organization_payload['branchCode'], 'companyRegister' => organization_payload['companyRegister'], diff --git a/site/spec/support/insee_sirene_api_mocks.rb b/site/spec/support/insee_sirene_api_mocks.rb index 4014cf0c9e..243d2838d9 100644 --- a/site/spec/support/insee_sirene_api_mocks.rb +++ b/site/spec/support/insee_sirene_api_mocks.rb @@ -1,20 +1,16 @@ # frozen_string_literal: true module INSEESireneAPIMocks - def insee_sirene_api_etablissement_valid_payload(siret:, full: false) - if full - read_json_fixture("insee/#{siret}.json") - else - { - 'header' => { - 'statut' => 200, - 'message' => 'OK' - }, - 'etablissement' => { - 'siren' => siret.first(9) - } + def insee_sirene_api_etablissement_valid_payload(siret:) + { + 'header' => { + 'statut' => 200, + 'message' => 'OK' + }, + 'etablissement' => { + 'siren' => siret.first(9) } - end + } end def insee_sirene_api_not_found_payload @@ -25,8 +21,4 @@ def insee_sirene_api_not_found_payload } } end - - def read_json_fixture(file) - JSON.parse(Rails.root.join('spec', 'fixtures', file).read) - end end From a5032f147523e60eb72bd04c5d17ce16a47c697e Mon Sep 17 00:00:00 2001 From: Thomas COMES Date: Thu, 6 Aug 2026 09:56:40 +0200 Subject: [PATCH 5/5] Expect the DATAPASS scope casing HubEE requires OAuth2 scopes are case-sensitive; the stub drifted to lowercase. --- site/spec/clients/hubee_api_authentication_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/spec/clients/hubee_api_authentication_spec.rb b/site/spec/clients/hubee_api_authentication_spec.rb index b10bbecfab..0466f38091 100644 --- a/site/spec/clients/hubee_api_authentication_spec.rb +++ b/site/spec/clients/hubee_api_authentication_spec.rb @@ -6,7 +6,7 @@ before do stub_request(:post, auth_url) - .with(body: 'grant_type=client_credentials&scope=datapass') + .with(body: 'grant_type=client_credentials&scope=DATAPASS') .to_return( status: 200, headers: { 'Content-Type' => 'application/json' },