<!-- gdoc: 1PGOT7hpW4K3dILMQis6jiqnQz9cc0XfU1a5iSKVcVE0 -->
# Gestion des consentements — Proposition

**Victor Lequay — Mars 2026 — Brouillon pour discussion**

---

## Constat

Aucun système ne suit les consentements. Les mandats sont des PDFs sur Google Drive, le référencement est dans nos têtes. Le ct-injector collecte tant que le fournisseur le permet — on découvre les expirations quand les données s'arrêtent.

**Risques concrets :**

- **Audit Enedis** — Enedis peut exiger la preuve de chaque mandat à tout moment. En cas de manquement : avertissement, suspension de l'accès SGE, résiliation du contrat GRD-T, voire signalement CNIL.
- **Audit GRDF** — GRDF peut demander la preuve du consentement par écrit à tout moment.
- **Collecte sans consentement** — Aujourd'hui, impossible de savoir combien de compteurs sont collectés sans mandat valide. C'est le risque #1.
- **Expirations silencieuses** — Les mandats Enedis expirent à 36 mois max. Sans suivi, on le découvre quand les données s'arrêtent.

---

## Cadre réglementaire

### Textes applicables

| Texte | Portée |
|-------|--------|
| **Code de l'énergie, Art. L341-4** | Obligation de consentement du consommateur pour l'accès tiers aux données de consommation électrique |
| **Décret n°2017-948 du 10 mai 2017** | Conditions d'accès tiers aux données Linky |
| **CNIL — Délibération n°2012-404** | Recommandation sur les compteurs communicants : les données fines de consommation sont des données personnelles |
| **CNIL — Pack de conformité "Compteurs communicants"** (~2018) | Cadre pratique de conformité pour les données Linky/Gazpar |
| **Contrat GRD-T Enedis** | Contrat obligatoire pour accéder au portail SGE. Fixe la durée max des mandats à **36 mois** (limite contractuelle, pas légale). |
| **Contrat ADICT GRDF** (version mai 2024) | Contrat obligatoire pour accéder aux données gaz via l'API GRDF |
| **Code Civil, Art. 2224** | Prescription quinquennale → conservation des preuves de consentement pendant **5 ans** après la fin du consentement |
| **RGPD, Art. 17(3)(b)** | Exception au droit à l'effacement pour obligation légale → permet de conserver les preuves d'audit |

### RGPD — Implications pour le consent-manager

**Les données de consommation énergétique sont des données personnelles** selon la CNIL. Les courbes de charge Linky (30 min) révèlent les habitudes de vie, la présence/absence, le nombre d'occupants. Même les index journaliers/mensuels associés à un PRM identifiable sont des données personnelles.

**Données personnelles dans le système :**

- `grantor_name`, `grantor_role` → données personnelles du signataire
- `meter_id` associé à un contrat → donnée indirectement personnelle

**Approche retenue :**

| Sujet | Règle |
|-------|-------|
| **Base légale** | Consentement explicite (Art. 6(1)(a) RGPD) pour l'accès tiers aux données de consommation |
| **Conservation des preuves** | 5 ans après fin du consentement (Art. 2224 Code Civil) |
| **Droit à l'effacement** | Anonymisation des champs personnels (`grantor_name` → `ANONYMISÉ`), conservation de l'enregistrement pour audit (Art. 17(3)(b) RGPD) |
| **Nettoyage automatique** | Cron mensuel : anonymiser les consentements dont `retention_until < now()` |
| **Registre des traitements** | Le consent-manager doit être déclaré dans le registre RGPD de l'entreprise |

---

## Modèles par fournisseur

### Enedis (électricité)

- **Identifiant** : PRM (Point de Référence Mesures)
- **Consentement** : Mandat signé (PDF) par le titulaire du contrat d'acheminement
- **Durée** : **36 mois max** (contrat GRD-T / CGU SGE). Si non précisé : **2 ans par défaut**
- **Temporalité** : Collecte possible immédiatement (déclaration ACCORD_CLIENT via SGE), mais Enedis peut demander la preuve du mandat à tout moment (~1 mois après activation, ou aléatoirement)
- **Prérequis** : Contrat GRD-T signé avec Enedis pour accéder au portail SGE
- **Conservation** : 5 ans pour traçabilité (Art. 2224 Code Civil)
- **Renouvellement** : Nouveau mandat signé obligatoire. Pas de tacite reconduction.
- **Audit** : Enedis exige le mandat original signé (identité, PRM, texte de consentement, finalité, durée, signature, date, info sur le droit de rétractation). Conséquences : avertissement → suspension SGE → résiliation GRD-T → signalement CNIL.

### GRDF (gaz)

- **Identifiant** : PCE (Point de Comptage et d'Estimation)
- **Consentement** : Autorisation avec **dates de début et de fin explicites** (pas un simple booléen)
- **Temporalité** : Consentement obligatoire **avant** l'accès aux données. Pas de collecte sans enrôlement.
- **Prérequis** : Contrat GRDF ADICT signé (version mai 2024, contrat "Multicanal")
- **Deux parcours de recueil** ([Atelier GRDF ADICT v1.8, juillet 2024](https://sites.grdf.fr/documents/44859/0/GRDF+ADICT_Atelier+fonctionnel+et+technique+_v1.8.pdf)) :
  1. **Parcours Tiers Direct** — Le tiers recueille le consentement lui-même, déclare le droit d'accès via l'API `declare-acces` (PUT). GRDF demande au titulaire de valider par email/SMS (délai 3j personne physique, 2j personne morale). GRDF contrôle les preuves a posteriori sur échantillon. → **Adapté B2B et parcs multiples.**
  2. **Parcours Client Connect** — Le tiers redirige le titulaire vers monespace.grdf.fr (POST vers le service GRDF). GRDF authentifie le titulaire, recueille le consentement pour le transfert des données, puis redirige vers le site du tiers. → **Adapté B2C et parcours digitalisés.** Le tiers reste responsable du consentement pour l'utilisation des données ; GRDF est responsable du consentement pour le transfert.
- **Dés-enrôlement** : Obligatoire quand le consentement expire ou est révoqué. Notification à GRDF sous **2 jours ouvrés**.
- **Audit** : GRDF peut demander la preuve du consentement à tout moment par écrit (Tiers Direct uniquement — avec Client Connect, GRDF détient la preuve de transfert).
- **Pas de webhooks** : GRDF ne notifie pas les changements de statut. Il faut **polling quotidien** via `droits-acces`.

### CPCU / Engie (chauffage urbain)

- **Identifiant** : N° client
- **Consentement** : Contrat commercial avec Engie. Pas de consentement individuel.
- **Accès** : API Nemo (Okta/SAML), inclus dans le contrat commercial
- **Note** : Si le sous-comptage individuel est déployé, le RGPD s'applique aux données traçables à une personne physique.

### Fraîcheur de Paris (refroidissement)

- **Identifiant** : Mnémonique (sous-station)
- **Consentement** : Contrat commercial + clé API. Pas de procédure publique.

### Futurs fournisseurs / capteurs tiers

- Granularité, forme et temporalité variables
- Le système doit être extensible sans modification majeure
- La table `provider_configs` permet d'ajouter un fournisseur sans changement de code

---

## Automatisation du recueil de consentement

### État des lieux par fournisseur

| Fournisseur | Mécanisme | Niveau d'automatisation | Webhooks | Polling nécessaire |
|-------------|-----------|------------------------|----------|-------------------|
| **Enedis DataConnect** | OAuth2 Authorization Code Grant | ✅ Entièrement automatisé | Non | Non (tokens + refresh) |
| **Enedis SGE** | SOAP avec `declarationAccordClient` | ⚠️ Semi-automatisé (autodéclaration) | Non | Oui |
| **GRDF Tiers Direct** | Recueil propre + API `declare-acces` | ⚠️ Semi-automatisé | Non | Oui (quotidien via `droits-acces`) |
| **GRDF Client Connect** | Redirect vers monespace.grdf.fr | ✅ Automatisé (OAuth2-like) | Non | Oui (quotidien via `droits-acces`) |
| **CPCU / Fraîcheur** | Contrat commercial | 🔒 Manuel (contractuel) | N/A | N/A |

**Constat important : aucun fournisseur ne propose de webhooks.** Les changements de statut doivent être détectés par polling ou par échec des appels API.

### Enedis DataConnect — Flux OAuth2

Enedis propose un vrai flux OAuth2 pour le consentement des clients C5 Linky via [DataConnect](https://datahub-enedis.fr/) :

```
1. Redirection vers Enedis :
   GET https://mon-compte-particulier.enedis.fr/dataconnect/v1/oauth2/authorize
     ?client_id={UUID}&response_type=code&state={state}
     &duration=P3Y&redirect_uri={callback_url}

2. L'utilisateur s'authentifie sur le portail Enedis et approuve

3. Enedis redirige vers notre callback :
   GET {redirect_uri}?code={code}&state={state}&usage_point_id={PRM}

4. Échange du code contre des tokens :
   POST https://gw.prd.api.enedis.fr/v1/oauth2/token
   → access_token (3h) + refresh_token + usage_point_id

5. Accès aux données via Bearer token :
   GET https://gw.prd.api.enedis.fr/v3/metering_data/daily_consumption
```

**Points clés :**
- Le PRM (`usage_point_id`) est retourné dans le callback — on sait directement quel compteur est concerné
- Le `refresh_token` permet un accès longue durée sans re-consentement (dans la limite du `duration`)
- L'utilisateur peut révoquer depuis son espace Enedis à tout moment — on le découvre quand les appels API échouent
- **Sandbox disponible** sur datahub-enedis.fr

**Note** : ct-injector utilise actuellement SGE (SOAP, pour tous les segments). DataConnect est une alternative pour les clients C5 Linky. Les deux approches coexistent.

### GRDF — Deux parcours API

#### Parcours Tiers Direct (B2B, parcs multiples)

```
1. Le tiers recueille le consentement (formulaire papier/digital)

2. Déclaration du droit d'accès :
   PUT /declare-acces
   → PCE, date_debut_droit_acces, date_fin_droit_acces, perimetre_de_donnees

3. GRDF demande au titulaire de valider (email/SMS)
   → Délai : 3 jours (personne physique), 2 jours (personne morale)
   → Le titulaire peut refuser (3 refus = blocage)

4. Vérification quotidienne (polling) :
   GET /droits-acces?pce={PCE}
   → etat_droit_acces, statut_controle_preuve
```

#### Parcours Client Connect (B2C, parcours digitalisé)

```
1. Le tiers appelle le service GRDF (POST)
   → GRDF affiche le service Client Connect au titulaire

2. Le titulaire se connecte sur monespace.grdf.fr
   → S'authentifie (ou crée un compte GRDF)
   → Choisit son PCE
   → Donne son consentement pour le transfert des données

3. GRDF redirige le titulaire vers le site du tiers
   → Le tiers est informé par un flux retour (callback)

4. GRDF confirme l'accès au Tiers
   → Le tiers peut accéder aux données du PCE (GET)
```

**Avantage** : GRDF détient la preuve du consentement au transfert → pas de contrôle de preuve a posteriori pour cette partie. Le tiers reste responsable du consentement à l'utilisation des données.

#### Statuts de l'API droits-acces

| Champ | Valeurs | Signification |
|-------|---------|---------------|
| `etat_droit_acces` | `Active` | Droit d'accès valide, données accessibles |
| `statut_controle_preuve` | `Preuve en attente` | ❌ Preuve non encore fournie — données inaccessibles |
| `statut_controle_preuve` | `Preuve Vérifiée KO` | ❌ Preuve rejetée par GRDF — correction nécessaire |

**Points clés :**
- Les deux parcours coexistent (source : [Atelier GRDF v1.8, juillet 2024](https://sites.grdf.fr/documents/44859/0/GRDF+ADICT_Atelier+fonctionnel+et+technique+_v1.8.pdf), pages 14-19)
- Le endpoint `droits-acces` est conçu pour un usage quotidien en cron — détection précoce des problèmes
- Notification de révocation à GRDF sous **2 jours ouvrés**
- Format de réponse : NDJSON (Newline Delimited JSON)
- Client Python : [lowatt/lowatt-grdf](https://github.com/lowatt/lowatt-grdf)

### Intégration dans consent-manager

Le consent-manager peut synchroniser son état avec les API fournisseurs :

**Cron de synchronisation (quotidien) :**

| Action | Fournisseur | Détail |
|--------|-------------|--------|
| Polling `droits-acces` | GRDF | Vérifier que `etat_droit_acces = Active` et `statut_controle_preuve` OK pour chaque PCE |
| Détection de révocation | Enedis DataConnect | Si le refresh token échoue → marquer le consentement comme REVOKED |
| Mise à jour du statut | Tous | Si le statut côté fournisseur diverge du statut local → mettre à jour + créer un événement d'audit |

**Flux de création de consentement avec API fournisseur :**

1. L'opérateur crée un consentement dans consent-manager (status = `IN_PROGRESS`)
2. Pour GRDF : consent-manager appelle `declare-acces` automatiquement
3. Pour Enedis DataConnect : consent-manager génère l'URL OAuth2, l'opérateur la transmet au client
4. Le callback Enedis met à jour le consentement (status → `OBTAINED`, stocke les tokens)
5. Pour Enedis SGE : l'opérateur déclare manuellement l'ACCORD_CLIENT sur le portail SGE

**Données supplémentaires à stocker par consentement (selon le fournisseur) :**

```sql
-- Champs optionnels pour l'intégration API fournisseur (JSONB sur consents)
-- Enedis DataConnect : access_token, refresh_token, token_expires_at, usage_point_id
-- GRDF : etat_droit_acces, statut_controle_preuve, date_debut_droit_acces, date_fin_droit_acces
-- Stockés dans un champ JSONB "provider_data" pour éviter de polluer le schéma principal
```

---

## Cycle de vie

<img src="https://drive.google.com/uc?id=1_n05uGDm273CTzlcOklcEybZT7KS0uWh" width="500">

### Statuts

| Statut | Couleur | Signification |
|--------|---------|---------------|
| `REQUIRED` | 🔴 Rouge | Consentement nécessaire, aucune action en cours |
| `IN_PROGRESS` | 🔵 Bleu | Processus d'obtention en cours |
| `OBTAINED` | 🟢 Vert | Consentement valide et actif |
| `EXPIRING_SOON` | 🟠 Orange | Expiration proche (< `alert_before_days`) |
| `EXPIRED` | 🔴 Rouge | Expiré — la collecte doit s'arrêter |
| `REVOKED` | 🔴 Rouge | Révoqué explicitement par le donneur |

### Transitions autorisées

```
REQUIRED ──────────→ IN_PROGRESS       (processus d'obtention lancé)
IN_PROGRESS ───────→ OBTAINED          (preuve reçue, consentement validé)
IN_PROGRESS ───────→ REQUIRED          (processus échoué / abandonné)
OBTAINED ──────────→ EXPIRING_SOON     (cron : expires_at - now < alert_before_days)
EXPIRING_SOON ─────→ EXPIRED           (cron : expires_at < now)
EXPIRING_SOON ─────→ OBTAINED          (renouvelé — nouvelle date d'expiration)
EXPIRED ───────────→ IN_PROGRESS       (renouvellement lancé)
OBTAINED ──────────→ REVOKED           (révocation explicite)
EXPIRING_SOON ─────→ REVOKED           (révocation explicite)
REVOKED ───────────→ REQUIRED          (nouveau cycle nécessaire)
```

### Garde-fous (enforced in code)

- Transition vers `OBTAINED` impossible sans `proof_url` (ou `proof_type = 'contract_ref'` pour CPCU/Fraîcheur)
- Transition vers `OBTAINED` impossible sans `valid_from` et `expires_at` (pour Enedis/GRDF)
- Seul le cron (`system/cron`) peut déclencher OBTAINED → EXPIRING_SOON et EXPIRING_SOON → EXPIRED

### Sous-étapes du workflow (champ `workflow_stage`)

Le statut `IN_PROGRESS` masque un processus multi-étapes. Un champ `workflow_stage` permet de suivre la progression sans ajouter de statuts de premier niveau.

**Enedis :**

`MANDATE_DRAFTED` → `MANDATE_SENT` → `MANDATE_SIGNED` → `MANDATE_UPLOADED` → `DECLARED_SGE` → (transition vers OBTAINED)

**GRDF :**

`ENROLLMENT_REQUESTED` → `ENROLLMENT_CONFIRMED` → (transition vers OBTAINED)

Cas d'erreur GRDF :
`ENROLLMENT_CONFIRMED` → `PROOF_REJECTED` (si `statut_controle_preuve = Preuve Vérifiée KO`)
`PROOF_REJECTED` → `ENROLLMENT_REQUESTED` (après correction du formulaire)

Données associées : identifiant compteur, fournisseur, entité/bâtiment, donneur (nom + rôle), responsable (assigned_to), dates (signature, début, fin/expiration), preuve (lien PDF/confirmation), statut.

---

## Fonctionnalités

### Essentielles

1. **CRUD consentements** — par compteur, par fournisseur, avec statut et dates
2. **Liens vers les preuves** — URL Google Drive (existant) ou upload S3 (futur). Tout consentement OBTAINED doit avoir une preuve liée.
3. **Alertes d'expiration** — notifications proactives N jours avant expiration, avec escalade
4. **Gate pour la collecte** — API interrogée par ct-injector avant chaque collecte. Mode `monitor` (log + anomalie) puis `enforce` (blocage).
5. **Vue d'ensemble** — dashboard par bâtiment / portefeuille, heatmap bâtiments × fournisseurs
6. **Audit trail enrichi** — historique de tous les changements (statut, champs, preuves), pas seulement les transitions de statut
7. **Couverture croisée** — comparaison consentements vs compteurs actifs dans ct-injector pour identifier les collectes sans consentement (risque #1)

### Souhaitables

8. **Workflow d'obtention** — suivi Kanban : mandat rédigé → envoyé → signé → uploadé → déclaré SGE
9. **Import en masse** — CSV/JSON de PRMs/PCEs → consentements créés avec traçabilité (`source: bulk_import`)
10. **Intégration chatbot** — "Quels consentements manquent pour le bâtiment X ?"
11. **Responsable par consentement** — champ `assigned_to` + escalade automatique si aucune action

---

## Scénario concret : nouveau client GRDF

> Un nouveau client souhaite utiliser nos services de suivi énergétique. Il a notre app et veut activer la collecte de données gaz.

> **Deux variantes possibles** selon le parcours choisi :
> - **Parcours Tiers Direct** (ci-dessous) : nous gérons le formulaire de consentement + déclaration API
> - **Parcours Client Connect** : nous redirigeons le client vers monespace.grdf.fr où GRDF recueille le consentement, puis callback vers notre app (plus simple côté preuve, mais dépendance au portail GRDF)

### Ce que voit le client dans l'app (Parcours Tiers Direct)

**1. Indication du manque de consentement**

Dans la vue bâtiment, le client voit :
- ✅ Électricité (PRM 12345) — données disponibles
- ❌ Gaz (PCE 98765) — consentement requis → [Donner mon consentement]

**2. Formulaire de consentement (digital, hébergé par nous)**

Le client remplit un formulaire conforme aux exigences GRDF (Tiers Direct = nous sommes responsables de la preuve) :

| Champ | Exemple |
|-------|---------|
| Identité | Jean Dupont, gestionnaire du bâtiment |
| PCE | 98765432101234 (pré-rempli si connu) |
| Périmètre des données | ☑ Consommations publiées ☑ Données contractuelles |
| Finalité | "Suivi énergétique dans le cadre du service Greenstratai" |
| Durée | Du 25/03/2026 au 25/03/2028 (24 mois) |
| Droit de rétractation | Texte légal + lien de révocation |
| Signature | Case à cocher horodatée + IP (ou signature électronique) |

**3. Confirmation**

> "Votre consentement a été enregistré. Nous déclarons votre autorisation auprès de GRDF. Les données seront disponibles sous 24-48h."

Le statut gaz passe de ❌ à ↻ (en cours), puis ✅ quand les données arrivent.

### Processus interne (flux technique)

```
Client (app)              consent-manager              GRDF API               ct-injector
    |                           |                          |                       |
    |-- Soumet le formulaire -->|                          |                       |
    |                           |                          |                       |
    |                           |-- 1. Crée le consent     |                       |
    |                           |   status: IN_PROGRESS    |                       |
    |                           |   workflow: ENROLLMENT_  |                       |
    |                           |            REQUESTED     |                       |
    |                           |   proof_type: digital_   |                       |
    |                           |              form        |                       |
    |                           |   proof_url: /forms/xyz  |                       |
    |                           |                          |                       |
    |                           |-- 2. POST /declare-acces |                       |
    |                           |   PCE, dates, périmètre->|                       |
    |                           |                          |                       |
    |                           |<-- 200 OK ---------------|                       |
    |                           |   workflow → ENROLLMENT_ |                       |
    |                           |             CONFIRMED    |                       |
    |                           |                          |                       |
    |<-- "En cours de ---------|                          |                       |
    |    validation"            |                          |                       |
    |                           |                          |                       |
    |        ... cron quotidien (quelques heures/jours) ...                        |
    |                           |                          |                       |
    |                           |-- 3. GET /droits-acces ->|                       |
    |                           |                          |                       |
    |                           |<-- etat: Active ---------|                       |
    |                           |   controle_preuve: OK    |                       |
    |                           |                          |                       |
    |                           |-- 4. status → OBTAINED   |                       |
    |                           |   event: status_change   |                       |
    |                           |                          |                       |
    |                           |                     5. GET /check/grdf/98765 --->|
    |                           |                          |     ← allowed: true   |
    |                           |                          |                       |
    |                           |                          |     6. Fetch data --->|
    |<-- "Données gaz ✅" -----|                          |                       |
```

### Cas d'erreur

**GRDF rejette la preuve** (détecté par le cron de polling) :
1. `droits-acces` retourne `statut_controle_preuve = Preuve Vérifiée KO`
2. consent-manager : `workflow_stage` → `PROOF_REJECTED`
3. Notification Zulip : "GRDF a rejeté la preuve pour PCE 98765 — correction nécessaire"
4. L'opérateur (`assigned_to`) contacte le client pour corriger le formulaire
5. Nouveau formulaire → nouveau `declare-acces` → cycle reprend

**Le client révoque depuis son espace GRDF** :
1. `droits-acces` retourne `etat_droit_acces ≠ Active`
2. consent-manager : `status` → `REVOKED`, événement d'audit créé
3. ct-injector : `GET /check` → `allowed: false` → arrêt de la collecte
4. Notification : "Consentement gaz révoqué pour PCE 98765"

**Preuve en attente prolongée** (> 7 jours) :
1. Cron détecte `statut_controle_preuve = Preuve en attente` depuis > 7j
2. Alerte à l'opérateur : "PCE 98765 — preuve en attente depuis 7 jours"
3. Escalade automatique si pas d'action (cf. échelle d'escalade)

### Ce que le formulaire de consentement implique

Le formulaire digital **est la preuve**. Il doit être :
- **Archivé** dès la soumission (PDF généré + stocké = `proof_url`)
- **Horodaté** (timestamp serveur + IP du signataire)
- **Conforme au modèle GRDF** (les champs requis sont définis par le contrat ADICT)
- **Disponible pour audit** (GRDF peut demander la preuve à tout moment)
- **Non modifiable après signature** (intégrité du document)

---

## Architecture proposée

### Vue d'ensemble

<img src="https://drive.google.com/uc?id=1G2b0VcZdWJQpmHu1kBsMnPp5eDOdXtg0" width="550">

### Stack

| Composant | Choix | Justification |
|-----------|-------|---------------|
| **Langage** | Go | Cohérent avec metainfodb, gedb, ct-injector-go. Binaire statique, déploiement simple. |
| **Base de données** | PostgreSQL | Relations consentement↔compteur↔bâtiment, requêtes complexes (expirations, agrégations), JSONB pour métadonnées flexibles |
| **Stockage documents** | Google Drive (Phase 1) → S3/MinIO (Phase 5) | PDFs signés, confirmations. Référencés par URL dans la BDD. |
| **API** | REST + JSON | Cohérent avec les autres services |
| **Machine à états** | `qmuntal/stateless` (Go) | Garde-fous, callbacks, export DOT pour documentation |
| **MCP** | TypeScript (comme les autres MCPs) | Accès via chatbot et Claude Code |
| **Déploiement** | Nomad (namespace mcp) | Même infra que les autres services |

### Pourquoi développer en interne ?

[Homeys](https://www.homeys.fr) propose un SaaS commercial de gestion des consentements énergétiques (Enedis, GRDF, CPCU) avec formulaires conformes, horodatage certifié et archivage. Ils ont passé les audits Enedis et GRDF.

**Raisons de développer notre propre système :**

- **Intégration profonde** avec notre stack existant (ct-injector, Hemisphere, GEDB, MCP, reporter)
- **Pas de dépendance SaaS** pour un système critique de conformité
- **Fournisseurs spécifiques** (Fraîcheur de Paris, futurs capteurs tiers) non couverts par les SaaS existants
- **Maîtrise des coûts** à notre échelle (~centaines de compteurs, pas milliers)
- **Données sensibles** — les preuves de consentement restent dans notre infrastructure

### Modèle de données

```sql
-- Consentement principal
CREATE TABLE consents (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    meter_id        TEXT NOT NULL,           -- PRM, PCE, n° client, mnémonique
    provider        TEXT NOT NULL,           -- enedis, grdf, cpcu, fraicheur, ...
    entity_id       TEXT,                    -- ID Hemisphere/GraphDB du bâtiment
    project_id      TEXT,                    -- ID du projet/programme

    status          TEXT NOT NULL DEFAULT 'REQUIRED',
    -- REQUIRED, IN_PROGRESS, OBTAINED, EXPIRING_SOON, EXPIRED, REVOKED

    workflow_stage  TEXT,                    -- Sous-étape du processus d'obtention
    -- Enedis : MANDATE_DRAFTED, MANDATE_SENT, MANDATE_SIGNED, MANDATE_UPLOADED, DECLARED_SGE
    -- GRDF : ENROLLMENT_REQUESTED, ENROLLMENT_CONFIRMED

    assigned_to     TEXT,                    -- Email du responsable de l'obtention/renouvellement

    grantor_name    TEXT,                    -- Qui a donné le consentement (donnée personnelle)
    grantor_role    TEXT,                    -- copropriétaire, gestionnaire, ...

    signed_at       TIMESTAMPTZ,            -- Date de signature
    valid_from      TIMESTAMPTZ,            -- Début de validité
    expires_at      TIMESTAMPTZ,            -- Fin de validité
    revoked_at      TIMESTAMPTZ,            -- Date de révocation (si applicable)

    proof_type      TEXT,                    -- pdf, api_confirmation, contract_ref, drive_link
    proof_url       TEXT,                    -- Lien vers le document (Drive URL ou S3)

    provider_data   JSONB,                   -- Données spécifiques au fournisseur :
    -- Enedis DataConnect : access_token, refresh_token, token_expires_at, usage_point_id
    -- GRDF : etat_droit_acces, statut_controle_preuve, date_debut/fin_droit_acces

    config_version  TEXT,                    -- Version du provider_config applicable
    anonymized_at   TIMESTAMPTZ,            -- Date d'anonymisation RGPD
    retention_until TIMESTAMPTZ,            -- Calculé : signed_at + 5 ans

    notes           TEXT,
    created_at      TIMESTAMPTZ DEFAULT now(),
    updated_at      TIMESTAMPTZ DEFAULT now()
);

-- Un seul consentement actif par compteur par fournisseur
-- Les EXPIRED/REVOKED restent comme historique
CREATE UNIQUE INDEX idx_consents_active_unique
    ON consents(meter_id, provider)
    WHERE status NOT IN ('EXPIRED', 'REVOKED');

-- Lookup rapide pour /check (endpoint ct-injector, haute fréquence)
CREATE INDEX idx_consents_provider_meter
    ON consents(provider, meter_id);

-- Requêtes d'expiration (cron)
CREATE INDEX idx_consents_expires_at
    ON consents(expires_at)
    WHERE status IN ('OBTAINED', 'EXPIRING_SOON');

-- Filtrage dashboard
CREATE INDEX idx_consents_entity_id ON consents(entity_id) WHERE entity_id IS NOT NULL;
CREATE INDEX idx_consents_project_id ON consents(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX idx_consents_status ON consents(status);

-- Historique des changements (audit trail enrichi)
CREATE TABLE consent_events (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    consent_id      UUID REFERENCES consents(id) ON DELETE CASCADE,
    event_type      TEXT NOT NULL,           -- status_change, proof_uploaded, field_updated, bulk_import
    old_status      TEXT,
    new_status      TEXT,
    changed_fields  JSONB,                   -- {"expires_at": {"old": "...", "new": "..."}}
    changed_by      TEXT NOT NULL,            -- Email de l'opérateur ou "system/cron"
    reason          TEXT,
    source          TEXT,                     -- api, cron, bulk_import, migration
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_consent_events_consent_id ON consent_events(consent_id);
CREATE INDEX idx_consent_events_created_at ON consent_events(created_at);

-- Configuration par fournisseur (versionnée)
CREATE TABLE provider_configs (
    id                       SERIAL PRIMARY KEY,
    provider                 TEXT NOT NULL,
    version                  TEXT NOT NULL,            -- "2024-05", "2026-01"
    effective_from           DATE NOT NULL,
    default_duration_months  INT,                      -- 24 pour Enedis, NULL pour contrats
    max_duration_months      INT,                      -- 36 pour Enedis
    alert_before_days        INT DEFAULT 60,           -- Alerte N jours avant expiration
    requires_pre_enrollment  BOOLEAN DEFAULT false,    -- true pour GRDF
    gate_mode                TEXT DEFAULT 'monitor',   -- monitor (log only) ou enforce (blocage)
    notes                    TEXT,
    UNIQUE(provider, version)
);
```

### API REST

| Méthode | Path | Description |
|---------|------|-------------|
| GET | `/consents` | Liste avec filtres (provider, status, entity_id, project_id, assigned_to, expiring_before). Pagination curseur. |
| GET | `/consents/:id` | Détail d'un consentement |
| POST | `/consents` | Créer un consentement |
| PUT | `/consents/:id` | Mettre à jour |
| PUT | `/consents/:id/status` | Changer le statut (+ raison → audit trail) |
| POST | `/consents/:id/proof` | Uploader un document de preuve |
| GET | `/consents/:id/proof` | Télécharger la preuve |
| GET | `/consents/:id/events` | Historique des changements |
| POST | `/consents/bulk` | Import en masse (CSV/JSON). Réponse : created/updated/errors avec détails par ligne. |
| GET | `/consents/summary` | Vue agrégée par projet/bâtiment |
| GET | `/consents/expiring` | Consentements expirant dans les N prochains jours |
| GET | `/consents/coverage` | Couverture croisée : consentements vs compteurs actifs dans ct-injector |
| GET | `/check/:provider/:meter_id` | Gate pour ct-injector : `{ "allowed": true/false, "status": "...", "expires_at": "..." }` |
| GET | `/providers` | Liste des fournisseurs configurés (avec version courante) |

### Intégration avec ct-injector

**Déploiement en deux temps :**

1. **Mode `monitor`** (Phase 3a) — ct-injector appelle `GET /check/{provider}/{meter_id}`. Si `allowed: false`, le fetch continue mais une anomalie `CONSENT_MISSING` est écrite dans GEDB. Pas de blocage. Permet de mesurer l'ampleur du problème.

2. **Mode `enforce`** (Phase 3b) — Si `allowed: false`, le fetch est annulé. Le mode est configurable par fournisseur dans `provider_configs.gate_mode`.

**Caching** : Index composite `(provider, meter_id)` → lookup <1ms en PostgreSQL. Cache Go en mémoire (TTL 5 min) pour éviter les requêtes répétées. En-tête `Cache-Control: max-age=300` pour ct-injector.

### Cron interne

| Fréquence | Action |
|-----------|--------|
| Quotidien | Transitions de statut : OBTAINED → EXPIRING_SOON (si `expires_at - now() < alert_before_days`) |
| Quotidien | Transitions de statut : EXPIRING_SOON → EXPIRED (si `expires_at < now()`) |
| Quotidien | Envoyer les alertes (Zulip) pour les consentements EXPIRING_SOON |
| Quotidien | Détection d'anomalies : compteurs dans ct-injector sans consentement valide |
| Quotidien | Synchronisation fournisseurs : polling GRDF `droits-acces`, vérification tokens Enedis DataConnect |
| Hebdomadaire | Rapport résumé : nombre de REQUIRED, IN_PROGRESS, EXPIRING_SOON, EXPIRED par projet |
| Mensuel | Nettoyage RGPD : anonymiser les consentements dont `retention_until < now()` |

---

## Notifications et escalade

### Canaux

| Déclencheur | Canal | Contenu |
|-------------|-------|---------|
| Passage en EXPIRING_SOON | Zulip (`#consent-alerts > expiring`) | Batch : "N consentements expirent dans 30j pour le projet X" avec liste et lien |
| Passage en EXPIRED | Zulip (`#consent-alerts > expired`) | Urgent : compteurs concernés + bâtiments |
| Gate ct-injector refusée | Zulip (`#consent-alerts > blocked`) | "Collecte bloquée pour PRM X — consentement EXPIRED/MISSING" |
| Digest hebdomadaire | Email aux chefs de projet | Couverture %, expirations du mois, items non résolus |
| Escalade (pas d'action sous 7j) | Zulip DM au team lead | "PRM X est EXPIRING_SOON depuis 7 jours sans action" |

### Échelle d'escalade

| Délai | Action |
|-------|--------|
| J+0 | EXPIRING_SOON : notification au responsable (`assigned_to`) |
| J+7 | Pas d'action : relance + notification au team lead |
| J+14 | Pas d'action : escalade chef de projet, flag dans le digest hebdo |
| J+21 | Pas d'action : escalade direction |
| Date d'expiration | EXPIRED : alerte à tous les intervenants, blocage collecte (si mode enforce) |

### Prévention de la fatigue d'alertes

- **Regrouper** les alertes par projet/bâtiment (pas une notification par consentement)
- **Chaque alerte contient** : ce qui s'est passé, pourquoi c'est important, quelle action prendre, lien direct
- **Digest pour l'informationnel**, temps réel uniquement pour les transitions critiques
- **Acquittement** : un opérateur peut acquitter une alerte (supprime des notifications actives) ou la reporter (re-alerte dans N jours)

---

## Besoins UI

> Cette section décrit les besoins fonctionnels de l'interface, pas son design. L'implémentation peut passer par reporter, un dashboard standalone, ou les deux.

### Vues essentielles

| Vue | Description | Priorité |
|-----|-------------|----------|
| **Vue portefeuille** | KPIs en haut (total, couverture %, expirant 30j, manquants) + cartes RAG par fournisseur | P1 |
| **Heatmap bâtiments × fournisseurs** | Grille : lignes = bâtiments, colonnes = fournisseurs, cellules = statut RAG (🟢🟠🔴⚪). Vision instantanée des trous. | P1 |
| **Liste des consentements** | Table filtrable/triable avec sélection multiple, badges de statut, actions en masse | P1 |
| **Détail consentement** | Toutes les infos + aperçu preuve + timeline audit trail + boutons d'action sur le statut | P1 |
| **Assistant d'import** | Upload → mapping colonnes → validation inline → aperçu → exécution | P1 |
| **Calendrier d'expiration** | Vue mensuelle avec les expirations en événements colorés par fournisseur | P2 |
| **Kanban d'obtention** | Colonnes = étapes du workflow, cartes = consentements en cours, avec assignation et durée dans l'étape | P2 |

### Code couleur / RAG

| Statut | Couleur | Icône | Signification |
|--------|---------|-------|---------------|
| OBTAINED | 🟢 Vert | ✓ | Valide |
| IN_PROGRESS | 🔵 Bleu | ↻ | En cours de traitement |
| EXPIRING_SOON | 🟠 Orange | ⚠ | Renouvellement nécessaire |
| REQUIRED | 🔴 Rouge | ✗ | Manquant — risque d'audit |
| EXPIRED | 🔴 Rouge | ✗ | Expiré — collecte doit cesser |
| REVOKED | 🔴 Rouge | ⊘ | Révoqué |

Utiliser couleur **ET** icônes/texte pour l'accessibilité (daltonisme).

### Points d'intégration dans les outils existants

- **Hemisphere (vue bâtiment)** : badge "Santé consentement" montrant le statut des compteurs du bâtiment
- **ct-injector (vue compte)** : statut consentement à côté de chaque compteur
- **GEDB (anomalie CONSENT_MISSING)** : lien direct vers le consentement dans consent-manager
- **Chatbot (MCP)** : "Quels consentements manquent pour le bâtiment X ?" → réponse formatée avec liens

### Rapports

| Rapport | Fréquence | Destinataire |
|---------|-----------|-------------|
| **Couverture par fournisseur** | À la demande | Opérateurs |
| **Préparation audit** | À la demande | Conformité — liste de toutes les preuves par fournisseur |
| **Prévision d'expirations** | Mensuel | Chefs de projet — 30/60/90 jours |
| **Tendance couverture** | Mensuel | Direction — évolution du % de couverture |
| **Compteurs sans consentement** | Hebdomadaire | Opérateurs — croisement avec ct-injector |

---

## Phasage

| Phase | Contenu | Dépendances |
|-------|---------|-------------|
| **1** | Service Go + API REST + PostgreSQL. Import des consentements Enedis existants (audit Drive) avec **liens vers les preuves Drive**. Endpoint `/coverage` pour identifier les collectes sans consentement. | PostgreSQL sur eu-central |
| **2** | MCP server + intégration chatbot. Dashboard via reporter (heatmap bâtiments × fournisseurs). | MCP déployé sur Nomad |
| **3a** | Gate ct-injector en **mode monitor** : log + anomalie GEDB `CONSENT_MISSING`, pas de blocage. | Modification ct-injector |
| **3b** | Gate ct-injector en **mode enforce** : blocage de la collecte si pas de consentement. | Validation terrain du mode monitor |
| **4** | Alertes d'expiration (Zulip). Escalade automatique. Digests hebdomadaires. | Alert-service ou envoi direct |
| **5** | Upload de preuves (S3/MinIO). Workflow d'obtention (Kanban). Intégration API fournisseurs : GRDF `declare-acces`/`droits-acces` automatique, callback Enedis DataConnect, cron de synchronisation. | Stockage objet déployé, accès API GRDF ADICT |

---

## Questions pour la discussion

| # | Question | Recommandation |
|---|----------|----------------|
| 1 | **Qui obtient les consentements ?** | Nous (Greenstratai), via le gestionnaire du bâtiment. Le champ `assigned_to` permet de suivre la responsabilité. |
| 2 | **Bloquer ou alerter ?** | Les deux, en séquence. Phase 3a : mode `monitor` (alerter + anomalie GEDB). Phase 3b : mode `enforce` (bloquer). Configurable par fournisseur. |
| 3 | **Stockage des preuves ?** | Phase 1 : URLs Google Drive (existant). Phase 5 : S3/MinIO pour les nouveaux uploads. Les deux coexistent via `proof_type`. |
| 4 | **RGPD** | Section dédiée ajoutée. Anonymisation après 5 ans, pas de suppression des enregistrements (exception Art. 17(3)(b)). Déclaration au registre. |
| 5 | **Priorité fournisseurs ?** | Enedis d'abord (risque d'audit le plus élevé, horloge 36 mois en cours). GRDF ensuite (pas de collecte sans enrôlement = blocage technique, pas seulement réglementaire). |
| 6 | **GRDF : Client Connect ou Tiers Direct ?** | Les deux coexistent (Atelier GRDF v1.8, juillet 2024). Client Connect = GRDF recueille la preuve de transfert (adapté B2C/digital). Tiers Direct = nous recueillons et contrôle a posteriori (adapté B2B/parcs multiples). Idéalement supporter les deux selon le contexte. |

### Questions ouvertes restantes

- **Volumétrie** : Combien de compteurs gère-t-on aujourd'hui ? (dimensionnement BDD, choix de pagination)
- **Multi-projet** : Un même compteur peut-il appartenir à plusieurs projets ? (impact sur la contrainte d'unicité)
- **Délégation** : Faut-il gérer des rôles (opérateur, chef de projet, admin) avec des permissions différentes ?
- **Enedis DataConnect vs SGE** : ct-injector utilise SGE (SOAP, tous segments). DataConnect (OAuth2) permettrait un flux de consentement entièrement automatisé pour les clients C5 Linky. Faut-il supporter les deux ? Migrer vers DataConnect à terme ?
- **GRDF : quel parcours privilégier ?** Les deux parcours (Tiers Direct et Client Connect) coexistent. Client Connect est plus adapté au B2C/digital, Tiers Direct au B2B/parcs multiples. Quel est notre cas d'usage principal ? Peut-on supporter les deux ?
