From 16698df9274bd14943ae9a0b7c8d83ae7a443ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Mon, 8 Apr 2024 15:26:54 +0100 Subject: [PATCH 1/8] Introduce CategorieJuridique model Same as Code NAF --- app/models/categorie_juridique.rb | 14 ++ config/categories_juridiques.yml | 262 ++++++++++++++++++++++++ spec/models/categorie_juridique_spec.rb | 11 + 3 files changed, 287 insertions(+) create mode 100644 app/models/categorie_juridique.rb create mode 100644 config/categories_juridiques.yml create mode 100644 spec/models/categorie_juridique_spec.rb diff --git a/app/models/categorie_juridique.rb b/app/models/categorie_juridique.rb new file mode 100644 index 000000000..bc0e52110 --- /dev/null +++ b/app/models/categorie_juridique.rb @@ -0,0 +1,14 @@ +class CategorieJuridique < StaticApplicationRecord + attr_accessor :code, + :libelle + + def self.all + @all ||= Rails.application.config_for(:categories_juridiques).map do |code, libelle| + new(code: code.to_s, libelle:) + end + end + + def id + code + end +end diff --git a/config/categories_juridiques.yml b/config/categories_juridiques.yml new file mode 100644 index 000000000..b07752d11 --- /dev/null +++ b/config/categories_juridiques.yml @@ -0,0 +1,262 @@ +--- +shared: + '1000': Entrepreneur individuel + '2110': Indivision entre personnes physiques + '2120': Indivision avec personne morale + '2210': Société créée de fait entre personnes physiques + '2220': Société créée de fait avec personne morale + '2310': Société en participation entre personnes physiques + '2320': Société en participation avec personne morale + '2385': Société en participation de professions libérales + '2400': Fiducie + '2700': Paroisse hors zone concordataire + '2900': Autre groupement de droit privé non doté de la personnalité morale + '3110': Représentation ou agence commerciale d'état ou organisme public étranger immatriculé au RCS + '3120': Société commerciale étrangère immatriculée au RCS + '3205': Organisation internationale + '3210': État, collectivité ou établissement public étranger + '3220': Société étrangère non immatriculée au RCS + '3290': Autre personne morale de droit étranger + '4110': Établissement public national à caractère industriel ou commercial doté d'un comptable public + '4120': Établissement public national à caractère industriel ou commercial non doté d'un comptable public + '4130': Exploitant public + '4140': Établissement public local à caractère industriel ou commercial + '4150': Régie d'une collectivité locale à caractère industriel ou commercial + '4160': Institution Banque de France + '5191': Société de caution mutuelle + '5192': Société coopérative de banque populaire + '5193': Caisse de crédit maritime mutuel + '5194': Caisse (fédérale) de crédit mutuel + '5195': Association coopérative inscrite (droit local Alsace Moselle) + '5196': Caisse d'épargne et de prévoyance à forme coopérative + '5202': Société en nom collectif + '5203': Société en nom collectif coopérative + '5306': Société en commandite simple + '5307': Société en commandite simple coopérative + '5308': Société en commandite par actions + '5309': Société en commandite par actions coopérative + '5370': Société de Participations Financières de Profession Libérale Société en commandite par actions (SPFPL SCA) + '5385': Société d'exercice libéral en commandite par actions + '5410': SARL nationale + '5415': SARL d'économie mixte + '5422': SARL immobilière pour le commerce et l'industrie (SICOMI) + '5426': SARL immobilière de gestion + '5430': SARL d'aménagement foncier et d'équipement rural (SAFER) + '5431': SARL mixte d'intérêt agricole (SMIA) + '5432': SARL d'intérêt collectif agricole (SICA) + '5442': SARL d'attribution + '5443': SARL coopérative de construction + '5451': SARL coopérative de consommation + '5453': SARL coopérative artisanale + '5454': SARL coopérative d'intérêt maritime + '5455': SARL coopérative de transport + '5458': SARL coopérative ouvrière de production (SCOP) + '5459': SARL union de sociétés coopératives + '5460': Autre SARL coopérative + '5470': Société de Participations Financières de Profession Libérale Société à responsabilité limitée (SPFPL SARL) + '5485': Société d'exercice libéral à responsabilité limitée + '5498': SARL unipersonnelle + '5499': Société à responsabilité limitée (sans autre indication) + '5505': SA à participation ouvrière à conseil d'administration + '5510': SA nationale à conseil d'administration + '5515': SA d'économie mixte à conseil d'administration + '5520': Fonds à forme sociétale à conseil d'administration + '5522': SA immobilière pour le commerce et l'industrie (SICOMI) à conseil d'administration + '5525': SA immobilière d'investissement à conseil d'administration + '5530': SA d'aménagement foncier et d'équipement rural (SAFER) à conseil d'administration + '5531': Société anonyme mixte d'intérêt agricole (SMIA) à conseil d'administration + '5532': SA d'intérêt collectif agricole (SICA) à conseil d'administration + '5542': SA d'attribution à conseil d'administration + '5543': SA coopérative de construction à conseil d'administration + '5546': SA de HLM à conseil d'administration + '5547': SA coopérative de production de HLM à conseil d'administration + '5548': SA de crédit immobilier à conseil d'administration + '5551': SA coopérative de consommation à conseil d'administration + '5552': SA coopérative de commerçants-détaillants à conseil d'administration + '5553': SA coopérative artisanale à conseil d'administration + '5554': SA coopérative (d'intérêt) maritime à conseil d'administration + '5555': SA coopérative de transport à conseil d'administration + '5558': SA coopérative ouvrière de production (SCOP) à conseil d'administration + '5559': SA union de sociétés coopératives à conseil d'administration + '5560': Autre SA coopérative à conseil d'administration + '5570': Société de Participations Financières de Profession Libérale Société anonyme à conseil d'administration (SPFPL SA à conseil d'administration) + '5585': Société d'exercice libéral à forme anonyme à conseil d'administration + '5599': SA à conseil d'administration (s.a.i.) + '5605': SA à participation ouvrière à directoire + '5610': SA nationale à directoire + '5615': SA d'économie mixte à directoire + '5620': Fonds à forme sociétale à directoire + '5622': SA immobilière pour le commerce et l'industrie (SICOMI) à directoire + '5625': SA immobilière d'investissement à directoire + '5630': Safer anonyme à directoire + '5631': SA mixte d'intérêt agricole (SMIA) + '5632': SA d'intérêt collectif agricole (SICA) + '5642': SA d'attribution à directoire + '5643': SA coopérative de construction à directoire + '5646': SA de HLM à directoire + '5647': Société coopérative de production de HLM anonyme à directoire + '5648': SA de crédit immobilier à directoire + '5651': SA coopérative de consommation à directoire + '5652': SA coopérative de commerçants-détaillants à directoire + '5653': SA coopérative artisanale à directoire + '5654': SA coopérative d'intérêt maritime à directoire + '5655': SA coopérative de transport à directoire + '5658': SA coopérative ouvrière de production (SCOP) à directoire + '5659': SA union de sociétés coopératives à directoire + '5660': Autre SA coopérative à directoire + '5670': Société de Participations Financières de Profession Libérale Société anonyme à Directoire (SPFPL SA à directoire) + '5685': Société d'exercice libéral à forme anonyme à directoire + '5699': SA à directoire (s.a.i.) + '5710': SAS, société par actions simplifiée + '5720': Société par actions simplifiée à associé unique ou société par actions simplifiée unipersonnelle + '5770': Société de Participations Financières de Profession Libérale Société par actions simplifiée (SPFPL SAS) + '5785': Société d'exercice libéral par action simplifiée + '5800': Société européenne + '6100': Caisse d'Épargne et de Prévoyance + '6210': Groupement européen d'intérêt économique (GEIE) + '6220': Groupement d'intérêt économique (GIE) + '6316': Coopérative d'utilisation de matériel agricole en commun (CUMA) + '6317': Société coopérative agricole + '6318': Union de sociétés coopératives agricoles + '6411': Société d'assurance à forme mutuelle + '6511': Sociétés Interprofessionnelles de Soins Ambulatoires + '6521': Société civile de placement collectif immobilier (SCPI) + '6532': Société civile d'intérêt collectif agricole (SICA) + '6533': Groupement agricole d'exploitation en commun (GAEC) + '6534': Groupement foncier agricole + '6535': Groupement agricole foncier + '6536': Groupement forestier + '6537': Groupement pastoral + '6538': Groupement foncier et rural + '6539': Société civile foncière + '6540': Société civile immobilière + '6541': Société civile immobilière de construction-vente + '6542': Société civile d'attribution + '6543': Société civile coopérative de construction + '6544': Société civile immobilière d' accession progressive à la propriété + '6551': Société civile coopérative de consommation + '6554': Société civile coopérative d'intérêt maritime + '6558': Société civile coopérative entre médecins + '6560': Autre société civile coopérative + '6561': SCP d'avocats + '6562': SCP d'avocats aux conseils + '6563': SCP d'avoués d'appel + '6564': SCP d'huissiers + '6565': SCP de notaires + '6566': SCP de commissaires-priseurs + '6567': SCP de greffiers de tribunal de commerce + '6568': SCP de conseils juridiques + '6569': SCP de commissaires aux comptes + '6571': SCP de médecins + '6572': SCP de dentistes + '6573': SCP d'infirmiers + '6574': SCP de masseurs-kinésithérapeutes + '6575': SCP de directeurs de laboratoire d'analyse médicale + '6576': SCP de vétérinaires + '6577': SCP de géomètres experts + '6578': SCP d'architectes + '6585': Autre société civile professionnelle + '6588': Société civile laitière + '6589': Société civile de moyens + '6595': Caisse locale de crédit mutuel + '6596': Caisse de crédit agricole mutuel + '6597': Société civile d'exploitation agricole + '6598': Exploitation agricole à responsabilité limitée + '6599': Autre société civile + '6901': Autre personne de droit privé inscrite au registre du commerce et des sociétés + '7111': Autorité constitutionnelle + '7112': Autorité administrative ou publique indépendante + '7113': Ministère + '7120': Service central d'un ministère + '7150': Service du ministère de la Défense + '7160': Service déconcentré à compétence nationale d'un ministère (hors Défense) + '7171': Service déconcentré de l'État à compétence (inter) régionale + '7172': Service déconcentré de l'État à compétence (inter) départementale + '7179': "(Autre) Service déconcentré de l'État à compétence territoriale" + '7190': Ecole nationale non dotée de la personnalité morale + '7210': Commune et commune nouvelle + '7220': Département + '7225': Collectivité et territoire d'Outre Mer + '7229': "(Autre) Collectivité territoriale" + '7230': Région + '7312': Commune associée et commune déléguée + '7313': Section de commune + '7314': Ensemble urbain + '7321': Association syndicale autorisée + '7322': Association foncière urbaine + '7323': Association foncière de remembrement + '7331': Établissement public local d'enseignement + '7340': Pôle métropolitain + '7341': Secteur de commune + '7342': District urbain + '7343': Communauté urbaine + '7344': Métropole + '7345': Syndicat intercommunal à vocation multiple (SIVOM) + '7346': Communauté de communes + '7347': Communauté de villes + '7348': Communauté d'agglomération + '7349': Autre établissement public local de coopération non spécialisé ou entente + '7351': Institution interdépartementale ou entente + '7352': Institution interrégionale ou entente + '7353': Syndicat intercommunal à vocation unique (SIVU) + '7354': Syndicat mixte fermé + '7355': Syndicat mixte ouvert + '7356': Commission syndicale pour la gestion des biens indivis des communes + '7357': Pôle d'équilibre territorial et rural (PETR) + '7361': Centre communal d'action sociale + '7362': Caisse des écoles + '7363': Caisse de crédit municipal + '7364': Établissement d'hospitalisation + '7365': Syndicat inter hospitalier + '7366': Établissement public local social et médico-social + '7367': Centre Intercommunal d'action sociale (CIAS) + '7371': Office public d'habitation à loyer modéré (OPHLM) + '7372': Service départemental d'incendie et de secours (SDIS) + '7373': Établissement public local culturel + '7378': Régie d'une collectivité locale à caractère administratif + '7379': "(Autre) Établissement public administratif local" + '7381': Organisme consulaire + '7382': Établissement public national ayant fonction d'administration centrale + '7383': Établissement public national à caractère scientifique culturel et professionnel + '7384': Autre établissement public national d'enseignement + '7385': Autre établissement public national administratif à compétence territoriale limitée + '7389': Établissement public national à caractère administratif + '7410': Groupement d'intérêt public (GIP) + '7430': Établissement public des cultes d'Alsace-Lorraine + '7450': 'Etablissement public administratif, cercle et foyer dans les armées ' + '7470': Groupement de coopération sanitaire à gestion publique + '7490': Autre personne morale de droit administratif + '8110': Régime général de la Sécurité Sociale + '8120': Régime spécial de Sécurité Sociale + '8130': Institution de retraite complémentaire + '8140': Mutualité sociale agricole + '8150': Régime maladie des non-salariés non agricoles + '8160': Régime vieillesse ne dépendant pas du régime général de la Sécurité Sociale + '8170': Régime d'assurance chômage + '8190': Autre régime de prévoyance sociale + '8210': Mutuelle + '8250': Assurance mutuelle agricole + '8290': Autre organisme mutualiste + '8310': Comité central d'entreprise + '8311': Comité d'établissement + '8410': Syndicat de salariés + '8420': Syndicat patronal + '8450': Ordre professionnel ou assimilé + '8470': Centre technique industriel ou comité professionnel du développement économique + '8490': Autre organisme professionnel + '8510': Institution de prévoyance + '8520': Institution de retraite supplémentaire + '9110': Syndicat de copropriété + '9150': Association syndicale libre + '9210': Association non déclarée + '9220': Association déclarée + '9221': Association déclarée d'insertion par l'économique + '9222': Association intermédiaire + '9223': Groupement d'employeurs + '9224': Association d'avocats à responsabilité professionnelle individuelle + '9230': Association déclarée, reconnue d'utilité publique + '9240': Congrégation + '9260': Association de droit local (Bas-Rhin, Haut-Rhin et Moselle) + '9300': Fondation + '9900': Autre personne morale de droit privé + '9970': Groupement de coopération sanitaire à gestion privée diff --git a/spec/models/categorie_juridique_spec.rb b/spec/models/categorie_juridique_spec.rb new file mode 100644 index 000000000..65cc4e4c1 --- /dev/null +++ b/spec/models/categorie_juridique_spec.rb @@ -0,0 +1,11 @@ +RSpec.describe CategorieJuridique do + describe '#find' do + subject(:find) { described_class.find(code) } + + let(:code) { '1000' } + + it 'works' do + expect(find.libelle).to eq('Entrepreneur individuel') + end + end +end From 53a329df29ddf21181707d189a6aec06523440b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Mon, 8 Apr 2024 11:36:34 +0100 Subject: [PATCH 2/8] Introduce Organization#categorie_juridique_label --- app/models/organization.rb | 8 ++++++++ spec/models/organization_spec.rb | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/models/organization.rb b/app/models/organization.rb index dc4e8e1ff..a49ebc538 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -22,6 +22,14 @@ def raison_sociale mon_compte_pro_payload['label'] end + def categorie_juridique_label + return unless insee_payload + + CategorieJuridique.find(insee_payload['etablissement']['uniteLegale']['categorieJuridiqueUniteLegale']).libelle + rescue ActiveRecord::RecordNotFound + nil + end + def self.ransackable_attributes(_auth_object = nil) %w[ siret diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 1e58d1b6a..5bbf45962 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -2,4 +2,20 @@ it 'has valid factories' do expect(build(:organization, siret: '21920023500014')).to be_valid end + + describe '#categorie_juridique_label' do + subject { organization.categorie_juridique_label } + + context 'with an organization with an insee payload' do + let(:organization) { build(:organization, siret: '13002526500013') } + + it { is_expected.to eq('Service central d\'un ministère') } + end + + context 'with an organization without an insee payload' do + let(:organization) { build(:organization) } + + it { is_expected.to be_nil } + end + end end From 1654920cf8032f0821127ac89e63858b106d693a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Thu, 4 Apr 2024 17:10:56 +0100 Subject: [PATCH 3/8] Add hubspot_private_app_access_token to production credentials --- config/credentials/production.yml.enc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index fcfa30fa7..bcaaa5220 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--0k+80OahCC3tzyrI--sc6g+ueqPUDukPZ7iI9Mzw== \ No newline at end of file +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--PifeVFqfNg7NRN4b--hsqnLRpPop2dNZ/ANYiEVw== \ No newline at end of file From f8c369b91502f8e6e3db3228e721c641689181fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Thu, 4 Apr 2024 17:12:36 +0100 Subject: [PATCH 4/8] Install hubspot-api-client --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Gemfile b/Gemfile index ce2594101..119d7656f 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'faraday' gem 'faraday-net_http' gem 'faraday-retry' gem 'good_job' +gem 'hubspot-api-client' gem 'kredis' gem 'omniauth' gem 'omniauth-rails_csrf_protection' diff --git a/Gemfile.lock b/Gemfile.lock index eac31eab1..331d1c7b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,6 +183,8 @@ GEM erubi (1.12.0) et-orbi (1.2.11) tzinfo + ethon (0.16.0) + ffi (>= 1.15.0) factory_bot (6.4.5) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) @@ -239,6 +241,9 @@ GEM http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) + hubspot-api-client (18.0.0) + json (~> 2.1, >= 2.1.0) + typhoeus (~> 1.4.0) i18n (1.14.4) concurrent-ruby (~> 1.0) i18n-tasks (1.0.13) @@ -540,6 +545,8 @@ GEM actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) + typhoeus (1.4.1) + ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) @@ -600,6 +607,7 @@ DEPENDENCIES guard guard-cucumber guard-rspec + hubspot-api-client i18n-tasks importmap-rails (~> 1.0) interactor From a007939c9fef3e0d572cef22b52ad7647b9f7ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Thu, 4 Apr 2024 17:14:26 +0100 Subject: [PATCH 5/8] Import HubspotAPI Imported from https://github.com/betagouv/datapass/blob/master/backend/app/services/hubspot_api.rb No tests, not really relevant for an API Client --- app/services/hubspot_api.rb | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/services/hubspot_api.rb diff --git a/app/services/hubspot_api.rb b/app/services/hubspot_api.rb new file mode 100644 index 000000000..e97d5be4a --- /dev/null +++ b/app/services/hubspot_api.rb @@ -0,0 +1,90 @@ +class HubspotAPI + def find_company_by_siret(siret, properties_to_include: []) + crm_api_client.companies.search_api.do_search( + body: build_search_body('siret', siret, properties_to_include:) + ).results.first + end + + def create_company(properties) + crm_api_client.companies.basic_api.create( + body: { + properties: + } + ) + end + + def update_company(company, properties) + crm_api_client.companies.basic_api.update( + company_id: company.id, + body: { + properties: + } + ) + end + + def find_contact_by_email(email, properties_to_include: []) + crm_api_client.contacts.search_api.do_search( + body: build_search_body('email', email, properties_to_include:) + ).results.first + end + + def create_contact(properties) + crm_api_client.contacts.basic_api.create( + body: { + properties: + } + ) + end + + def update_contact(contact, properties) + crm_api_client.contacts.basic_api.update( + contact_id: contact.id, + body: { + properties: + } + ) + end + + def add_contact_to_company(contact, company) + crm_api_client.associations.batch_api.create( + from_object_type: 'Contacts', + to_object_type: 'Companies', + body: { + inputs: [ + { + from: { id: contact.id }, + to: { id: company.id }, + type: 'contact_to_company' + } + ] + } + ) + end + + private + + def build_search_body(property_name, value, properties_to_include: []) + { + filterGroups: [ + { + filters: [ + { + propertyName: property_name, + operator: 'EQ', + value: + } + ] + } + ], + properties: properties_to_include + } + end + + def crm_api_client + api_client.crm + end + + def api_client + @api_client ||= Hubspot::Client.new(access_token: Rails.application.credentials.hubspot_private_app_access_token!) + end +end From d53aac22e6eed853818c3bed34c2664ffbe3527b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Mon, 8 Apr 2024 13:09:51 +0100 Subject: [PATCH 6/8] CRM is an acronym --- config/initializers/inflections.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 8ee401e59..b9cd81cba 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -17,6 +17,7 @@ inflect.acronym 'GDPR' inflect.acronym 'INSEE' inflect.acronym 'NAF' + inflect.acronym 'CRM' inflect.acronym 'HubEE' inflect.acronym 'DC' From 73a8a18a5af90660a5ee4dde6a95b0c416feb4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Mon, 8 Apr 2024 11:36:40 +0100 Subject: [PATCH 7/8] Introduce RegisterOrganizationWithContactsOnCRMJob --- ...r_organization_with_contacts_on_crm_job.rb | 198 ++++++++++++++++++ ...anization_with_contacts_on_crm_job_spec.rb | 64 ++++++ 2 files changed, 262 insertions(+) create mode 100644 app/jobs/register_organization_with_contacts_on_crm_job.rb create mode 100644 spec/jobs/register_organization_with_contacts_on_crm_job_spec.rb diff --git a/app/jobs/register_organization_with_contacts_on_crm_job.rb b/app/jobs/register_organization_with_contacts_on_crm_job.rb new file mode 100644 index 000000000..ed30ba995 --- /dev/null +++ b/app/jobs/register_organization_with_contacts_on_crm_job.rb @@ -0,0 +1,198 @@ +class RegisterOrganizationWithContactsOnCRMJob < ApplicationJob + attr_reader :authorization_request + + def perform(authorization_request_id) + @authorization_request = AuthorizationRequest.find_by(id: authorization_request_id) + + return unless authorization_request + + crm_company = find_or_create_company_on_crm + + valid_contacts.each do |contact| + add_contact_to_company( + find_or_create_contact_on_crm(contact), + crm_company + ) + end + end + + private + + def create_contact_on_crm(contact) + crm_client.create_contact( + email: contact.email, + firstname: contact.given_name, + lastname: contact.family_name, + phone: contact.phone_number, + type_de_contact: humanize_contact_types(extract_contact_type(contact)), + bouquet_s__associe_s_: extract_bouquet(:contact) + ) + end + + def create_company_on_crm + crm_client.create_company( + siret: organization.siret, + name: organization.raison_sociale, + categorie_juridique: organization.categorie_juridique_label, + n_datapass: authorization_request.id.to_s, + bouquets_utilises: extract_bouquet(:company) + ) + end + + def find_or_create_company_on_crm + crm_company = crm_client.find_company_by_siret(organization.siret, properties_to_include: company_properties_to_retrieve) + + update_multi_attributes_on_company(crm_company) if crm_company + + crm_company || + create_company_on_crm + end + + def find_or_create_contact_on_crm(contact) + crm_contact = crm_client.find_contact_by_email(contact.email, properties_to_include: contact_properties_to_retrieve) + + update_multi_attributes_on_contact(crm_contact, contact) if crm_contact + + crm_contact || + create_contact_on_crm(contact) + end + + def add_contact_to_company(crm_contact, crm_company) + crm_client.add_contact_to_company(crm_contact, crm_company) + end + + def valid_contacts + authorization_request.contacts.select { |contact| + valid_contact_types.include?(contact.type.to_sym) + } << authorization_request.applicant + end + + def valid_contact_types + %i[contact_metier contact_technique responsable_technique responsable_traitement delegue_protection_donnees] + end + + # rubocop:disable Metrics/AbcSize + def update_multi_attributes_on_company(crm_company) + datapass_ids = crm_company.properties['n_datapass'] + + datapass_ids = if datapass_ids.present? && datapass_ids.split(';').map(&:to_i).exclude?(authorization_request.id) + datapass_ids << "; #{authorization_request.id}" + else + authorization_request.id.to_s + end + + bouquets_utilises = crm_company.properties['bouquets_utilises'] + + if bouquets_utilises.present? && bouquets_utilises.exclude?(extract_bouquet(:company)) + bouquets_utilises << "; #{extract_bouquet(:company)}" + else + bouquets_utilises = extract_bouquet(:company) + end + + crm_client.update_company( + crm_company, { + n_datapass: datapass_ids, + bouquets_utilises: + } + ) + end + # rubocop:enable Metrics/AbcSize + + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + def update_multi_attributes_on_contact(crm_contact, contact) + contact_types = crm_contact.properties['type_de_contact'] + current_content_type = extract_contact_type(contact) + + if contact_types.present? && contact_types.exclude?(humanize_contact_types(contact.type)) + contact_types << "; #{humanize_contact_types(current_content_type)}" + else + contact_types = humanize_contact_types(current_content_type) + end + + bouquets_utilises = crm_contact.properties['bouquet_s__associe_s_'] + + if bouquets_utilises.present? && bouquets_utilises.exclude?(extract_bouquet(:contact)) + bouquets_utilises << "; #{extract_bouquet(:contact)}" + else + bouquets_utilises = extract_bouquet(:contact) + end + + crm_client.update_contact( + crm_contact, { + type_de_contact: contact_types, + bouquet_s__associe_s_: bouquets_utilises + } + ) + end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + + def company_properties_to_retrieve + %w[ + n_datapass + bouquet_s__associe_s_ + ] + end + + def contact_properties_to_retrieve + %w[ + type_de_contact + bouquets_utilises + ] + end + + def humanize_contact_types(type) + case type + when 'contact_metier' + 'Contact Métier' + when 'responsable_technique', 'contact_technique' + 'Contact Technique' + when 'delegue_protection_donnees' + 'Délégué à la protection des données' + when 'demandeur' + 'Demandeur' + when 'responsable_traitement' + 'Responsable de traitement' + end + end + + # rubocop:disable Metrics/MethodLength + def extract_bouquet(kind) + case authorization_request.type + when 'AuthorizationRequest::APIEntreprise' + if kind == :company + 'ENTREPRISE' + else + 'API Entreprise' + end + when 'AuthorizationRequest::APIParticulier' + if kind == :company + 'PARTICULIER' + else + 'API Particulier' + end + when 'AuthorizationRequest::FranceConnect' + if kind == :company + 'FRANCE_CONNECT' + else + 'FranceConnect' + end + end + end + # rubocop:enable Metrics/MethodLength + + def extract_contact_type(contact) + if contact.is_a?(User) + 'demandeur' + else + contact.type + end + end + + def organization + @organization ||= authorization_request.organization + end + + def crm_client + @crm_client ||= HubspotAPI.new + end +end diff --git a/spec/jobs/register_organization_with_contacts_on_crm_job_spec.rb b/spec/jobs/register_organization_with_contacts_on_crm_job_spec.rb new file mode 100644 index 000000000..c09d240c0 --- /dev/null +++ b/spec/jobs/register_organization_with_contacts_on_crm_job_spec.rb @@ -0,0 +1,64 @@ +RSpec.describe RegisterOrganizationWithContactsOnCRMJob do + subject { job_instance.perform_now } + + let(:job_instance) { described_class.new(authorization_request_id) } + + let(:hubspot_api) { instance_double(HubspotAPI) } + let(:entity) { OpenStruct.new(id: 42, properties: {}) } + + context 'when authorization_request is not found' do + let(:authorization_request_id) { -1 } + + it 'does not raises an error' do + expect { subject }.not_to raise_error + end + end + + context 'when authorization_request is found' do + let(:authorization_request) { create(:authorization_request, :api_entreprise, :validated) } + let(:authorization_request_id) { authorization_request.id } + + before do + allow(HubspotAPI).to receive(:new).and_return(hubspot_api) + + allow(hubspot_api).to receive_messages(add_contact_to_company: true, create_company: entity, create_contact: entity) + + %i[ + update_company + update_contact + ].each do |method| + allow(hubspot_api).to receive(method).and_return(true) + end + end + + context 'when there is no company nor contact on Hubspot' do + before do + %i[ + find_company_by_siret + find_contact_by_email + ].each do |method| + allow(hubspot_api).to receive(method).and_return(nil) + end + end + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end + + context 'when there is a company and at least one contact on Hubspot' do + before do + %i[ + find_company_by_siret + find_contact_by_email + ].each do |method| + allow(hubspot_api).to receive(method).and_return(entity) + end + end + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end + end +end From da5539c5c0f9ce8f4531ffd74623c7b7d63c644c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Delmaire?= Date: Mon, 8 Apr 2024 16:14:32 +0100 Subject: [PATCH 8/8] APIEntrepriseNotifier#validated calls RegisterOrganizationWithContactsOnCRMJob --- app/notifiers/api_entreprise_notifier.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/notifiers/api_entreprise_notifier.rb b/app/notifiers/api_entreprise_notifier.rb index 5bbc3bb77..3682ba88b 100644 --- a/app/notifiers/api_entreprise_notifier.rb +++ b/app/notifiers/api_entreprise_notifier.rb @@ -6,6 +6,10 @@ class APIEntrepriseNotifier < ApplicationNotifier # rubocop:enable Lint/EmptyBlock end + def validated(_params) + RegisterOrganizationWithContactsOnCRMJob.perform_later(authorization_request.id) + end + def submitted(_params) Instruction::AuthorizationRequestMailer.with( authorization_request: