Aller au contenu

ADR-0020 — Identité client, annuaire et anti-doublon à l'instigation

Statut Accepté — 2026-06-19 (archivage d'un client vide livré ; fusion : ouverte)
Portée Modèle Tenant (+ nom), instigation POST /api/demandes/pour-compte, nouvel GET /api/clients, écran Cabinet (senior)
Décision Un client reste un tenant (clé tenant_schema) ; on lui ajoute un nom d'affichage, un annuaire senior, et on rend la création d'un nouveau client explicite (fin du get-or-create silencieux qui fabriquait des doublons par typo)
Réf. ADR-0005 — Tenant = groupe ; ADR-0006 — Personas staff cross-tenant ; ADR-0007 — Vues par persona & instigation

🎯 À retenir

Un client = un tenant (tenant_schema, clé immuable) ; il n'y a pas d'entité Client. On garde ce modèle minimal mais on comble trois manques repérés en QA senior : (1) un nom lisible (Tenant.nom, raison sociale) en plus de l'identifiant technique ; (2) un annuaire clients côté senior (nom · tenant · e-mails de contact · nb de demandes), dérivé des données existantes ; (3) l'instigation n'invente plus un client : une typo du tenant donnait un client fantôme sans projet ; désormais on choisit un client existant, et créer un nouveau est un acte confirmé.

Contexte

Repéré en QA (persona senior). Le senior dispose d'une table « Équipe du cabinet » (staff : nom · e-mail · persona) mais d'aucune vue équivalente pour les clients. Pire : le formulaire « Nouvelle demande pour un client » (instigation, ADR-0007) prenait un champ tenant libre, et le backend faisait un get-or-create — un tenant inconnu était créé en silence. Une faute de frappe (client-tst au lieu de client-test) fabriquait donc un client en double, sans projet, indistinguable à l'œil de l'original.

Cause de fond : un client est modélisé de façon minimaleTenant.tenant_schema (clé unique), sans raison sociale ni e-mail attaché. Les e-mails clients ne vivent que figés dans chaque demande (Demande.created_by_email, ADR-0013). On ne pouvait donc ni lister les clients, ni les retrouver, ni se prémunir des doublons.

Décision

  • D-CLI-1 — Un client = un tenant. On garde le modèle minimal : la clé d'identité est tenant_schema (string unique, immuable). Pas d'entité Client séparée.
  • D-CLI-2 — Nom d'affichage. Tenant.nom (raison sociale, optionnel) est ajouté comme labeltenant_schema reste la clé. NULL = clients d'avant (on affiche alors le tenant). Colonne déclarée dans _COLONNES_AJOUTEES (sinon 500 prod).
  • D-CLI-3 — Instigation explicite (anti-doublon). Le formulaire propose en premier de choisir un client existant (liste). Créer un nouveau client est un acte confirmé : pour-compte exige confirmer_nouveau=true (+ client_nom) pour un tenant inconnu, sinon 409 (« choisissez un client existant, ou confirmez la création »). Fin du get-or-create silencieux.
  • D-CLI-4 — Annuaire clients (senior). GET /api/clients (réservé actuaire_senior, comme GET /api/equipe) : par tenant → nom, e-mails de contact (distincts, dérivés de created_by_email), nb de demandes, dernière activité. Données dérivées — aucune table nouvelle hormis Tenant.nom. Affiché sous le Cabinet, jumeau d'« Équipe du cabinet ».

Conséquences

  • Positif. Le senior voit ses clients et les choisit au lieu de retaper un identifiant : le doublon par typo disparaît. Le nom rend l'annuaire lisible sans casser la clé technique. Coût modèle minimal (une colonne).
  • Limite assumée. Un client fraîchement instigué (avant toute soumission client) peut n'avoir aucun e-mail de contact (l'instigation ne connaît pas l'e-mail) — l'annuaire l'affiche alors sans contact. Et tenant_schema reste non renommable (c'est la clé) ; seul le nom se corrige.

Déclencheur de réexamen (pré-enregistré) — le nettoyage reste ouvert

  • Archivage d'un client VIDE — livré (slice 2). POST /api/clients/{tenant}/archiver (réversible, senior, refusé si le client porte des demandes) masque le doublon par typo de l'annuaire actif et du sélecteur, sans rien perdre.
  • Fusion d'un client portant des demandes — OUVERTE. Merger = déplacer les demandes (et leurs dossiers/justifications/journal) d'un tenant vers un autre en préservant l'audit immuable (ADR-0008) — non trivial, et rare depuis la prévention. À traiter au cas par cas / one-off (script journalisé) plutôt qu'en fonctionnalité, sauf si le volume le justifie.
  • Raison sociale comme donnée Quantis. Si l'identité client doit venir de l'IdP (YODICORE/SSO) plutôt que d'une saisie cabinet, re-statuer (le nom deviendrait un miroir de la fiche Quantis).