# Modèle Physique de Données — SchotzimmO > **Source de vérité physique** des bases de données SchotzimmO. Document **transversal** à toute la plateforme. Ce document complète le modèle conceptuel (diagrammes ER des `design.md`) en décrivant le **détail physique** : tables, colonnes, types MariaDB, nullabilité, clés, index, relations et règles. > > **Architecture à deux niveaux de base** (voir `socle-plateforme/design.md`) : > - **Base Socle** (centrale, unique) : authentification, référentiel des réseaux et leurs informations de connexion, abonnements, catalogue des modules/packs, administration, KPI d'usage remontés par réseau. **Aucune donnée métier produite par les mandataires.** > - **Base Réseau** (une par réseau) : toutes les données métier cloisonnées d'un réseau — profil mandataire, parrainage/hiérarchie, et les données des Modules (Carnet d'adresses, KMS, CRM, Agenda, Facturation…). > > Le document grandit **module par module** : les sections Socle et Réseau sont posées en premier, puis une sous-section Base Réseau est ajoutée pour chaque Module. --- ## Conventions générales - **SGBD** : MariaDB / MySQL, via le provider EF Core `Pomelo.EntityFrameworkCore.MySql`. - **Moteur & encodage** : toutes les tables en **InnoDB**, charset **`utf8mb4`**, collation **`utf8mb4_unicode_ci`**. - **Nommage** : tables en **`snake_case` pluriel** ; colonnes en **`snake_case`**. Mapping entité C# (PascalCase) vers table/colonne assuré par EF Core. - **Clé primaire** : colonne **`id`**, type **`INT UNSIGNED`**, **`AUTO_INCREMENT`**, sauf exception documentée (ex. `mandataire_profiles.account_id` réutilise l'identifiant du socle). - **Clés étrangères** : nommées **`_id`**. Toute FK **au sein d'une même base** est une vraie contrainte FK indexée. Les références **entre bases différentes** (ex. `account_id` du socle utilisé dans une base réseau) ne sont **pas** des contraintes FK MariaDB : ce sont des **références logiques** (mêmes valeurs d'identifiant), dont la cohérence est assurée **applicativement**. - **Référence inter-bases** : `account_id` est généré par la Base Socle (`accounts.id`) et **réutilisé tel quel** comme identifiant du mandataire dans sa Base Réseau. Il n'y a jamais de jointure SQL traversant deux bases. - **Dates & heures** : type **`DATETIME`**, valeurs stockées en **UTC** ; horodatage de création par défaut `CURRENT_TIMESTAMP`. - **Chaînes** : **`VARCHAR(n)`** dimensionné (e-mail 255, nom/prénom 100…) ; **`TEXT`** pour le texte libre long. - **Booléens** : **`TINYINT(1)`** (0/1), mappés sur `bool` C#. - **Énumérations** : **`VARCHAR`** à valeurs contraintes applicativement (ex. `role`, `status`), plutôt que le type `ENUM` natif. - **Montants** : **`DECIMAL(12,2)`**. Distances/quantités : `DECIMAL` dimensionné selon le Module. - **Secrets** : mots de passe et jetons stockés **hachés**. **Aucun secret de connexion** (chaîne de connexion d'une base réseau) n'est stocké en clair en base : la Base Socle ne stocke qu'une **clé logique** de connexion, résolue vers un secret fourni par configuration. - **Suppressions** : par défaut physiques. Les cas de conservation (rétrogradation d'abonnement) reposent sur la présence des données et l'état d'accès calculé. ### Légende des tableaux de colonnes - **Clé** : `PK` = clé primaire, `FK` = clé étrangère (même base), `FK*` = référence logique inter-bases (non contrainte), `UK` = unicité. - **Null** : `NON` = `NOT NULL`, `OUI` = nullable. --- # PARTIE A — Base Socle (centrale, unique) Référence : `socle-plateforme/design.md`. Cette base ne contient aucune donnée métier produite par les mandataires. Vue d'ensemble : | Table | Rôle | |---|---| | `networks` | Référentiel des réseaux + clé logique de connexion à leur base réseau + paramétrage | | `accounts` | Comptes (authentification minimale) ; pivot d'identité, rattaché à un réseau | | `packs` | Formules d'abonnement (Starter / Pro / Premium) | | `module_catalog` | Catalogue des modules et pack requis pour chacun | | `subscriptions` | Abonnement courant d'un compte à un pack | | `password_resets` | Jetons de réinitialisation de mot de passe (hachés) | | `network_usage_kpis` | Indicateurs d'usage remontés par chaque réseau (pas de donnée nominative) | --- ### Table `networks` Référentiel des réseaux. Chaque réseau pointe vers sa propre base de données via une **clé logique de connexion** et porte son paramétrage de personnalisation. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant du réseau | | `code` | `VARCHAR(40)` | NON | UK | — | Code stable du réseau (ex. `iad`, `demo`) | | `name` | `VARCHAR(100)` | NON | UK | — | Libellé affiché (ex. « IAD ») | | `db_connection_key` | `VARCHAR(60)` | NON | UK | — | Clé logique résolvant la chaîne de connexion de la base réseau via la configuration (jamais la chaîne elle-même) | | `theme_settings` | `TEXT` | OUI | — | `NULL` | Paramétrage de personnalisation (thème, libellés) au format JSON | | `is_active` | `TINYINT(1)` | NON | — | `1` | Réseau ouvert à l'inscription si actif et initialisé | | `is_provisioned` | `TINYINT(1)` | NON | — | `0` | Base réseau initialisée (schéma + données de référence) | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | **Index** : `PRIMARY (id)`, `UNIQUE (code)`, `UNIQUE (name)`, `UNIQUE (db_connection_key)`. **Relations** : `1 — N` vers `accounts.network_id` (même base). Référencé logiquement par toutes les tables des bases réseau. **Données de référence (seed)** : `iad` (« IAD ») et `demo` (réseau de démonstration / tests). **Règles** : un réseau n'est ouvert à l'inscription que si `is_active = 1` ET `is_provisioned = 1` (onboarding réseau mené par l'Administrateur au préalable). --- ### Table `accounts` Comptes des mandataires — **authentification minimale uniquement**. Le profil métier et le parrainage vivent dans la Base Réseau (`mandataire_profiles`). L'`id` est réutilisé comme `account_id` côté réseau. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant du compte ; réutilisé comme `account_id` dans la base réseau | | `network_id` | `INT UNSIGNED` | NON | FK | — | Réseau de rattachement (non modifiable après création) | | `email` | `VARCHAR(255)` | NON | UK | — | Adresse e-mail (login), normalisée en minuscules. Unique sur toute la plateforme | | `password_hash` | `VARCHAR(255)` | NON | — | — | Empreinte du mot de passe (salée) ; jamais en clair | | `role` | `VARCHAR(20)` | NON | — | `'mandataire'` | Rôle : `mandataire` ou `admin` | | `is_active` | `TINYINT(1)` | NON | — | `1` | Compte actif (désactivation = `0`) | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | **Index** : `PRIMARY (id)`, `UNIQUE (email)`, `INDEX (network_id)`. **Relations** : `network_id` vers `networks.id` (même base). `id` référencé logiquement par `mandataire_profiles.account_id` dans la base réseau. **Règles** : - L'e-mail est unique sur l'ensemble de la plateforme (un e-mail = un seul compte, un seul réseau). - Un rôle `admin` est un administrateur SchotzimmO (transverse) ; il n'est pas rattaché aux données métier d'un réseau. - La désactivation d'un compte doit être propagée applicativement vers le profil réseau correspondant. --- ### Table `packs` Formules d'abonnement. Trois packs ordonnés par niveau, **inclusifs**. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant du pack | | `pack_key` | `VARCHAR(20)` | NON | UK | — | Clé stable : `starter`, `pro`, `premium` | | `label` | `VARCHAR(50)` | NON | — | — | Libellé affiché | | `level` | `TINYINT UNSIGNED` | NON | UK | — | Niveau d'inclusion : `0` < `1` < `2` | | `is_free` | `TINYINT(1)` | NON | — | `0` | Pack gratuit (vrai pour Starter) | **Index** : `PRIMARY (id)`, `UNIQUE (pack_key)`, `UNIQUE (level)`. **Données de référence (seed)** : `starter` (level 0, gratuit), `pro` (level 1), `premium` (level 2). --- ### Table `module_catalog` Catalogue des modules et pack minimal requis. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant de l'entrée catalogue | | `module_key` | `VARCHAR(40)` | NON | UK | — | Clé stable du module (ex. `carnet-adresses`, `kms`) | | `label` | `VARCHAR(80)` | NON | — | — | Libellé affiché | | `required_pack_id` | `INT UNSIGNED` | NON | FK | — | Pack minimal débloquant le module | | `is_active` | `TINYINT(1)` | NON | — | `1` | Module proposé/activé sur la plateforme | **Index** : `PRIMARY (id)`, `UNIQUE (module_key)`, `INDEX (required_pack_id)`. **Relations** : `required_pack_id` vers `packs.id`. **Données de référence (seed)** : `carnet-adresses`, `kms`, `facturation` vers `starter` ; `crm`, `agenda` vers `pro`. --- ### Table `subscriptions` Abonnement d'un compte à un pack. Un seul abonnement actif par compte. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant de l'abonnement | | `account_id` | `INT UNSIGNED` | NON | FK | — | Compte abonné (même base socle) | | `pack_id` | `INT UNSIGNED` | NON | FK | — | Pack souscrit | | `starts_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Début d'effet (UTC) | | `ends_at` | `DATETIME` | OUI | — | `NULL` | Fin d'effet (UTC) ; `NULL` = sans échéance | | `status` | `VARCHAR(10)` | NON | — | `'actif'` | `actif` ou `expire` | **Index** : `PRIMARY (id)`, `INDEX (account_id)`, `INDEX (pack_id)`, `INDEX (account_id, status)`. **Relations** : `account_id` vers `accounts.id` ; `pack_id` vers `packs.id` (même base). **Règles** : au plus un abonnement `actif` par compte ; rétrogradation conserve les données réseau (consultation seule déduite de la présence de données). --- ### Table `password_resets` Jetons de réinitialisation de mot de passe, stockés **hachés**, à usage unique et durée de vie limitée. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant du jeton | | `account_id` | `INT UNSIGNED` | NON | FK | — | Compte concerné | | `token_hash` | `VARCHAR(255)` | NON | — | — | Empreinte du jeton | | `expires_at` | `DATETIME` | NON | — | — | Échéance d'expiration (UTC) | | `used` | `TINYINT(1)` | NON | — | `0` | Jeton déjà consommé | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | **Index** : `PRIMARY (id)`, `INDEX (account_id)`, `INDEX (token_hash)`. **Relations** : `account_id` vers `accounts.id`. --- ### Table `network_usage_kpis` Indicateurs d'usage **remontés par chaque réseau** vers le socle pour les tableaux de bord d'administration. Contient des **agrégats non nominatifs** (jamais de donnée métier individuelle ; pas de jointure inter-bases). | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant de la mesure | | `network_id` | `INT UNSIGNED` | NON | FK | — | Réseau concerné | | `metric_key` | `VARCHAR(60)` | NON | — | — | Clé de l'indicateur (ex. `nb_mandataires_actifs`, `ca_total`) | | `metric_value` | `DECIMAL(14,2)` | NON | — | — | Valeur agrégée | | `period` | `VARCHAR(7)` | OUI | — | `NULL` | Période éventuelle au format `AAAA-MM` | | `collected_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de remontée (UTC) | **Index** : `PRIMARY (id)`, `INDEX (network_id)`, `INDEX (network_id, metric_key, period)`. **Relations** : `network_id` vers `networks.id`. **Règles** : alimentée par un processus de remontée par réseau ; ne contient aucune donnée individuelle de mandataire. --- ## Diagramme physique — Base Socle ```mermaid erDiagram networks ||--o{ accounts : "network_id" networks ||--o{ network_usage_kpis : "network_id" accounts ||--|| subscriptions : "account_id" packs ||--o{ subscriptions : "pack_id" packs ||--o{ module_catalog : "required_pack_id" accounts ||--o{ password_resets : "account_id" ``` --- # PARTIE B — Base Réseau (une par réseau) Référence : `socle-plateforme/design.md`. Chaque réseau dispose d'une instance de ce schéma. Toutes les tables sont **implicitement cloisonnées** : la base entière appartient à un seul réseau, il n'y a donc **pas** de colonne `network_id` dans les tables réseau (le cloisonnement est physique). Les identifiants `account_id` proviennent de la Base Socle (`FK*`, référence logique). Vue d'ensemble (Socle réseau ; les Modules ajouteront leurs tables) : | Table | Rôle | |---|---| | `mandataire_profiles` | Profil métier du mandataire ; pivot du parrainage (auto-référence `sponsor_account_id`) | | `declared_members` | Membres déclarés (parrain/filleul) non encore inscrits | | `affiliation_requests` | Demandes de confirmation d'affiliation entre mandataires inscrits | --- ### Table `mandataire_profiles` Profil métier d'un mandataire au sein de son réseau. La clé primaire **réutilise l'`account_id`** de la Base Socle (pas d'auto-incrément). Porte l'**arbre de parrainage** via l'auto-référence `sponsor_account_id`. | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `account_id` | `INT UNSIGNED` | NON | PK, FK* | — | Identifiant du compte (provient de `accounts.id` du socle) | | `first_name` | `VARCHAR(100)` | NON | — | — | Prénom | | `last_name` | `VARCHAR(100)` | NON | — | — | Nom | | `network_member_id` | `VARCHAR(50)` | NON | — | — | Identifiant du mandataire propre à son réseau (obligatoire) | | `legal_status` | `VARCHAR(30)` | OUI | — | `NULL` | Statut juridique (ex. `auto-entrepreneur`, `eirl`) | | `siren` | `VARCHAR(14)` | OUI | — | `NULL` | Numéro SIREN/RSAC, optionnel | | `sponsor_account_id` | `INT UNSIGNED` | OUI | FK | `NULL` | Parrain (auto-référence vers `mandataire_profiles.account_id`) ; `NULL` = racine | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | **Index** : `PRIMARY (account_id)`, `UNIQUE (network_member_id)`, `INDEX (sponsor_account_id)`. **Relations** : - `account_id` : référence logique vers `accounts.id` (Base Socle), non contrainte en SQL. - `sponsor_account_id` vers `mandataire_profiles.account_id` (auto-référence, même base réseau) : **vraie FK**, intégrité du parrainage garantie par la base. **Règles** : - Comme la base est propre au réseau, parrain et filleul sont **nécessairement** du même réseau (cloisonnement physique). - Absence de cycle vérifiée applicativement à l'écriture (parcours ascendant). - Au plus un `sponsor_account_id` validé par mandataire. --- ### Table `declared_members` Membres déclarés dans l'arbre de parrainage mais **non encore inscrits**. Sans donnée métier. Convertis en `mandataire_profiles` lors du Rapprochement_Email à l'inscription (le compte étant alors créé côté socle). | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant du membre déclaré | | `email` | `VARCHAR(255)` | NON | — | — | E-mail normalisé, clé de Rapprochement_Email | | `first_name` | `VARCHAR(100)` | OUI | — | `NULL` | Prénom déclaré (si connu) | | `last_name` | `VARCHAR(100)` | OUI | — | `NULL` | Nom déclaré (si connu) | | `declared_by_account_id` | `INT UNSIGNED` | NON | FK | — | Mandataire ayant déclaré (référence `mandataire_profiles.account_id`) | | `relation` | `VARCHAR(10)` | NON | — | — | Lien déclaré : `parrain` ou `filleul` | | `status` | `VARCHAR(10)` | NON | — | `'declare'` | `declare` (supposé) ou `invite` (invitation envoyée) | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | **Index** : `PRIMARY (id)`, `INDEX (declared_by_account_id)`, `INDEX (email)`. **Relations** : `declared_by_account_id` vers `mandataire_profiles.account_id` (même base). **Règles** : clôturé/supprimé lors de la conversion en `mandataire_profiles` par Rapprochement_Email. --- ### Table `affiliation_requests` Demandes de **confirmation d'affiliation** entre deux mandataires déjà inscrits (cas de « perte de lien »). | Colonne | Type | Null | Clé | Défaut | Description | |---|---|---|---|---|---| | `id` | `INT UNSIGNED` | NON | PK | auto | Identifiant de la demande | | `requester_account_id` | `INT UNSIGNED` | NON | FK | — | Mandataire initiant la demande | | `target_account_id` | `INT UNSIGNED` | NON | FK | — | Mandataire dont l'approbation est requise | | `proposed_relation` | `VARCHAR(10)` | NON | — | — | Lien proposé : `parrain` ou `filleul` | | `status` | `VARCHAR(12)` | NON | — | `'en_attente'` | `en_attente`, `approuvee` ou `refusee` | | `created_at` | `DATETIME` | NON | — | `CURRENT_TIMESTAMP` | Date de création (UTC) | | `resolved_at` | `DATETIME` | OUI | — | `NULL` | Date d'approbation/refus (UTC) | **Index** : `PRIMARY (id)`, `INDEX (requester_account_id)`, `INDEX (target_account_id)`, `INDEX (status)`. **Relations** : `requester_account_id` et `target_account_id` vers `mandataire_profiles.account_id` (même base). **Règles** : l'approbation ne doit pas créer de cycle (vérifié à la résolution). Le cloisonnement réseau est garanti par construction (base propre au réseau). --- ## Diagramme physique — Base Réseau (socle réseau) ```mermaid erDiagram mandataire_profiles ||--o| mandataire_profiles : "sponsor_account_id" mandataire_profiles ||--o{ declared_members : "declared_by_account_id" mandataire_profiles ||--o{ affiliation_requests : "requester / target" ``` > **Note inter-bases** : `mandataire_profiles.account_id` correspond à `accounts.id` de la Base Socle (référence logique `FK*`, jamais une FK SQL ni une jointure traversant deux bases). --- ## Sections à venir (ajoutées au fil des Modules — tables de la Base Réseau) - **Carnet d'adresses** *(à venir)* - **KMS** *(à venir)* - **CRM** *(à venir)* - **Agenda** *(à venir)* - **Facturation** *(à venir)*