Annuaire

Référence technique du domaine Annuaire — entreprises, établissements, contacts, KYB Pappers/INSEE, segmentation, géocodage.

Annuaire

Périmètre : référentiel d'entreprises clientes/prospects, d'établissements (sites opérationnels), de personnes (contacts), enrichissement KYB via Pappers/INSEE, segmentation commerciale et géocodage.

Les écrans vivent sous /workspace/{companies,establishments,persons,enseignes} et les routes API sont à plat sous /api/workspace/{companies,establishments,persons,segments} (le préfixe /annuaire/ n'existe pas). Source de cette page : docs/07-domain-annuaire.md.

Vocabulaire & entités

TermeEntitéDéfinition
EntrepriseCompanyEntité légale identifiée par SIREN. Source de vérité légale.
ÉtablissementEstablishmentSite physique identifié par SIRET — unité d'exploitation où les équipements sont installés.
PersonnePersonContact humain rattaché à une Company et/ou un Establishment. Sa fonction est le champ libre Person.jobTitle (pas d'entité dédiée).
EnseigneEnseigneMarque commerciale fédérant plusieurs établissements (ex : « Carrefour Market »). Portée par Establishment.enseigneId.
SegmentSegmentClassification commerciale org-scoped portée par l'établissement (ex : « GMS », « Horeca »). Portée par Establishment.segmentId.
Code NAF / NACECompany.activityCodeChamp de l'entreprise, pas une entité.

L'ancienne entité Activity (activité org-scoped rattachée à un Establishment) n'existe plus : le référentiel métier est désormais ActivityScope, un arbre porté par une société du groupe (GroupCompany) et administré dans Paramètres → Scopes d'activité. Il ne relève plus de l'annuaire — cf. 3.plateforme/2.modele-de-donnees.md.

Le modèle complet est dans docs/04-data-model.md §4. L'identité légale (SIREN/SIRET/nom/adresse) est toujours lue via Company/Establishment ; les modules CRM et Ventes ne font que référencer ces entités.

Écrans

ÉcranRouteContenu
Liste entreprises/workspace/companiesDataTable (SIREN, nom, ville, segments, nb établissements) + filtres.
Création entreprise/workspace/companies/newSaisie SIREN → autocomplete KYB (Pappers, fallback INSEE) ; saisie manuelle possible.
Import entreprises/workspace/companies/importImport CSV d'entreprises (création/mise à jour par SIREN).
Fiche entreprise/workspace/companies/[id]Onglets : Synthèse KYB, Établissements, Contacts, Opportunités, Affaires LLD, Factures, Fichiers, Commentaires, Audit.
Liste établissements/workspace/establishmentsDataTable + fiche détaillée (assets, maintenance, transport).
Création établissement/workspace/establishments/newSaisie d'un établissement (rattachement entreprise, adresse, segment, enseigne).
Liste personnes/workspace/personsDataTable des contacts (rattachement entreprise/établissement).
Création personne/workspace/persons/newSaisie d'un contact (identité, jobTitle, opt-in marketing).
Enseignes/workspace/enseignesCRUD enseignes (code, label) — partagé avec le CRM (cf. module CRM).

Modèle de données (points clés)

  • Company peut exister sans Establishment (prospect étranger ou micro-entreprise), mais un siège ou établissement principal est requis à la première commande/contrat.
  • Person est rattaché à 0 ou 1 Company et 0 ou 1 Establishment.
  • Suppression d'une Company interdite si Customer/Order/Invoice/LeaseContract actifs — soft delete uniquement.
  • Suppression d'un Establishment interdite si des assets y sont en CURRENT_LOCATION actifs.
  • Person.optInMarketing conditionne l'inclusion dans les exports Brevo (campagnes marketing).
  • Pas de données RGPD-sensitives sur les personnes morales ; RGPD strict sur Person.
  • enrichedAt, enrichmentSource (String?), rawEnrichment (JSONB) sur Company.
  • Unicité SIREN : index UNIQUE PARTIEL WHERE "deletedAt" IS NULL (migration recyclebin_partial_unique), hors schéma Prisma. Index Prisma déclarés sur Company : (organizationId, name), (organizationId, deletedAt). Même schéma d'unicité partielle pour Establishment (SIRET) et Enseigne/Segment (code/label).

API

Toutes les routes exigent une permission annuaire.* (vérifiée côté API et UI). Les routes sont à plat : aucun préfixe /annuaire/.

Entreprises (companies)

MéthodeRoutePermission
GET/api/workspace/companiesannuaire.company:read
POST/api/workspace/companiesannuaire.company:create
POST/api/workspace/companies/with-enrichmentannuaire.company:create
POST/api/workspace/companies/importannuaire.company:create
GET/api/workspace/companies/[id]annuaire.company:read
PATCH/api/workspace/companies/[id]annuaire.company:update
DELETE/api/workspace/companies/[id]annuaire.company:delete
GET/api/workspace/companies/[id]/delete-previewannuaire.company:delete
GET/api/workspace/companies/[id]/dirigeantsannuaire.company:read

Établissements (establishments)

MéthodeRoutePermission
GET/api/workspace/establishmentsannuaire.establishment:read
POST/api/workspace/establishmentsannuaire.establishment:create
GET/api/workspace/establishments/[id]annuaire.establishment:read
PATCH/api/workspace/establishments/[id]annuaire.establishment:update
DELETE/api/workspace/establishments/[id]annuaire.establishment:delete
GET/api/workspace/establishments/[id]/delete-previewannuaire.establishment:delete
POST/api/workspace/establishments/[id]/set-headquartersannuaire.establishment:update

Personnes (persons)

MéthodeRoutePermission
GET/api/workspace/personsannuaire.person:read
POST/api/workspace/personsannuaire.person:create
POST/api/workspace/persons/ensureannuaire.person:create
GET/api/workspace/persons/[id]annuaire.person:read
PATCH/api/workspace/persons/[id]annuaire.person:update

Segments (segments)

MéthodeRoutePermission
GET/api/workspace/segmentsannuaire.establishment:read
POST/api/workspace/segmentsannuaire.establishment:create
PATCH/api/workspace/segments/[id]annuaire.establishment:update
DELETE/api/workspace/segments/[id]annuaire.establishment:update

KYB (Pappers / INSEE)

MéthodeRoutePermission
GET/api/workspace/kyb/lookup?siren=annuaire.company:create
GET/api/workspace/kyb/searchannuaire.company:create
GET/api/workspace/kyb/enrich?siren=annuaire.company:create
GET/api/workspace/kyb/establishmentsannuaire.establishment:read
POST/api/workspace/kyb/import-establishmentannuaire.establishment:create
Les enseignes (Enseigne) sont un référentiel partagé avec le CRM : leur CRUD est exposé sous /api/workspace/crm/enseignes (lecture crm.opportunity:read, écriture crm.settings:update), pas sous l'annuaire. Voir le module CRM.

Exemple de référence d'endpoint :

GET/api/workspace/kyb/enrichAuth

Enrichissement KYB complet d'un SIREN : entreprise + établissements + dirigeants, pour alimenter la sélection « au cas par cas » à la création d'entreprise. company est null si le SIREN est introuvable. Permission annuaire.company:create.

Query

siren
string required
SIREN à 9 chiffres (validé Luhn).

Requête

curl -s "$API/api/workspace/kyb/enrich?siren=$SIREN" \
  -H "Cookie: $SESSION"

Réponse

{
  "company": { "name": "", "siren": "", "activityCode": "" },
  "establishments": [{ "siret": "", "city": "" }],
  "dirigeants": [{ "fullName": "", "role": "" }]
}

Workflows

Enrichissement KYB

Déclencheur manuel (création d'entreprise avec SIREN)
  │
  ├─→ kyb.service.ts → getKybProvider() (sélection via KYB_PROVIDER, défaut `mock`)
  │      ├─ lookupBySiren(siren)     → KybCompany | null     (route /kyb/lookup)
  │      ├─ enrichBySiren(siren)     → company + …           (route /kyb/enrich)
  │      ├─ listEstablishments(siren)→ établissements[]      (route /kyb/establishments)
  │      └─ getDirigeants(siren)     → dirigeants[]
  │
  └─→ persistance à la création via /companies/with-enrichment
        enrichedAt = now() · enrichmentSource · rawEnrichment = payload

Le fournisseur KYB est abstrait derrière getKybProvider() et sélectionné par KYB_PROVIDER (défaut mock ; pappers en KYB_PROVIDER_MODE=live avec PAPPERS_API_TOKEN). Champs persistés sur Company : enrichedAt, enrichmentSource, rawEnrichment (JSONB), activityCode, etc.

Cible (non implémenté) : fallback automatique vers INSEE Sirene V3 si le fournisseur principal échoue, cache 7 jours par SIREN et job d'enrichissement périodique. Le code actuel n'expose qu'un appel synchrone via le fournisseur configuré, sans cache ni job.

Import multi-établissements

  1. Recherche SIREN (autocomplete KYB ou saisie directe).
  2. Le fournisseur renvoie la liste des établissements (/kyb/establishments).
  3. Wizard de sélection : l'utilisateur coche les établissements à importer.
  4. POST /kyb/import-establishment { siren, sirets? } : crée/réutilise la Company puis, par SIRET, findOrCreateEstablishmentBySiret (rapprochement SIRET, idempotent, pas de doublon). Sans sirets, seul le siège est importé.

Géocodage adresse

Establishment porte latitude/longitude (Decimal(10,7), nullables), alimentés lors de la sélection/import KYB.

Cible (non encore implémentée) : géocodage automatique des adresses sans coordonnées (MapTiler / Nominatim) via un job dédié. Il n'existe pas de service de géocodage ni de champ geocodeFailed dans le schéma actuel.

Détection et merge de doublons

Cible (non encore implémentée) : détection de doublons par trigrammes pg_trgm, écran de résolution et fusion (merge) de fiches entreprises. Aucune de ces fonctions n'existe en code aujourd'hui (pas de route companies/merge, pas de job detect-duplicates, pas d'écran « doublons »). À la création, la seule garde active est l'unicité unique(organizationId, siren).

Règles métier

  • SIREN unique par organisation via index unique partiel WHERE "deletedAt" IS NULL (les fiches soft-deleted ne bloquent pas une recréation du même SIREN).
  • Validation SIREN/SIRET (Luhn) via shared/utils/siret.ts (exemption La Poste). vatNumber est une chaîne libre (≤ 20 car.), sans regex ni vérification VIES.
  • Listes paginées (max 100 par page), recherche serveur avec debounce côté UI.
  • Suppression : soft delete uniquement si entité référencée (voir contraintes modèle ci-dessus).
  • Person.optInMarketing : seuls les contacts opt-in sont exportés vers Brevo.

Intégrations

Pappers

Fournisseur KYB. Clé PAPPERS_API_TOKEN, activé via KYB_PROVIDER=pappers + KYB_PROVIDER_MODE=live. Défaut mock (fixtures), sélection centralisée dans server/lib/kyb/.

Cibles (non implémentées) : fallback INSEE Sirene V3, géocodage MapTiler / Nominatim et vérification TVA intra-UE VIES. Aucune de ces intégrations n'est présente en code à ce jour.