# Contrat d’échange monprojet — version 1

Page publique et Hectarion. Un e-mail en fin de conversation, une pièce jointe signée, une fiche projet prospect.

## Message

- Destinataire : la boîte configurée côté Hectarion (`MONPROJET_BOITE`).
- Sujet : `[monprojet] Projet de {prénom}`. Si le prénom est absent : `[monprojet] Projet`.
- Corps : résumé lisible par un humain. Hectarion ne s’en sert pas pour créer la fiche.
- Une seule pièce jointe, nommée exactement `monprojet.json` (la casse du nom n’est pas exigée).

## Enveloppe `monprojet.json`

```json
{
  "version": 1,
  "id": "uuid",
  "emis_le": "2026-10-07T12:00:00+02:00",
  "fiche": {},
  "transcription": [],
  "signature": "hex"
}
```

| Champ | Règle |
|---|---|
| `version` | Nombre entier `1`. Toute autre valeur est refusée. |
| `id` | UUID. Minuscules recommandées. Un identifiant déjà reçu est refusé (anti-rejeu), y compris si la fiche a été écartée après une signature valide. |
| `emis_le` | Date ISO 8601 avec fuseau (`Z` ou `+02:00`). Refus si elle a plus de 7 jours ou plus de 15 minutes dans le futur. La chaîne exacte entre dans la signature. |
| `fiche` | Objet. Voir plus bas. |
| `transcription` | Liste ordonnée de messages. |
| `signature` | HMAC-SHA256 hexadécimal, 64 caractères. |

## Signature

Chaîne signée, sans espace autour des barres :

`version|id|emis_le|sha256hex(canonique(fiche) + canonique(transcription))`

- `version` est le texte `1`.
- `id` et `emis_le` sont les chaînes exactes du fichier.
- `canonique(fiche)` et `canonique(transcription)` sont collés l’un à l’autre, puis passés à SHA-256 (hex minuscule).
- La signature est HMAC-SHA256 de cette chaîne, clé secrète partagée, hex minuscule.
- Comparaison côté Hectarion : `hash_equals`.

JSON canonique :

- Objet : clés triées par ordre binaire, y compris dans les objets imbriqués. Un objet vide est `{}`.
- Liste : ordre conservé. Une liste vide est `[]`.
- Pas d’espace. Unicode et barres `/` non échappés.
- Les nombres suivent l’encodage JSON après lecture (les zéros de queue ne comptent pas). Les deux côtés parsent le JSON, puis réencodent. Ils ne signent pas une sous-chaîne brute du fichier.

Algorithme de référence :

```
canonique(valeur) :
  si liste : "[" + canonique de chaque élément, séparés par "," + "]"
  si objet : clés triées, "{" + cle JSON + ":" + canonique(valeur) séparés par "," + "}"
  sinon : JSON de la valeur
chaine = "1" + "|" + id + "|" + emis_le + "|" + sha256hex(canonique(fiche) + canonique(transcription))
signature = hmac_sha256_hex(cle, chaine)
```

## Fiche

Champs obligatoires : `commune_insee`, `commune_libelle`, `budget`, `consentement.le`, `consentement.politique_version`. Les autres sont facultatifs. Une clé inconnue est ignorée à l’enregistrement ; elle reste couverte par la signature.

Texte : balises retirées, entités HTML décodées, octet nul retiré. Une valeur trop longue est refusée, pas tronquée en silence.

| Champ | Type | Borne |
|---|---|---|
| `prenom` | texte | 80 |
| `commune_insee` | code INSEE, 5 chiffres ou `2A` / `2B` | obligatoire |
| `commune_libelle` | texte | 120, obligatoire |
| `budget` | code fermé | obligatoire |
| `type_projet`, `foyer`, `horizon`, `terrain_possede`, `mode_contact`, `fin_conversation` | code fermé | |
| `chambres` | code fermé | |
| `email` | adresse | 180 |
| `telephone` | texte | 40 |
| `autres_communes` | liste (20 max) de textes ou de `{insee, libelle}` | libellé 120 |
| `ouvert_propositions` | booléen, ou `oui` / `non` | |
| `financement.apport`, `financement.pret` | nombre ≥ 0 | |
| `description` | texte | 4000 |
| `analyse.resume`, `analyse.motivation` | texte | 2000 |
| `analyse.freins` | liste de textes | 12 × 300 |
| `analyse.maturite` | code fermé | |
| `niveau` | `1`, `2` ou `3` | |
| `suivi.campagne` | texte | 120 |
| `suivi.accroche` | texte | 200 |
| `suivi.format` | code fermé | |
| `consentement.le` | ISO 8601 avec fuseau | obligatoire |
| `consentement.politique_version` | texte | 40, obligatoire |

Une valeur fermée inconnue est enregistrée `autre`. La valeur reçue, une fois le HTML retiré, est conservée à part et affichée comme « Valeur reçue ».

## Transcription

Liste, 200 messages maximum. Chaque message : `role` (20), `texte` (4000), `horodatage` (ISO, facultatif, 40). Rôles recommandés : `visiteur`, `assistant`.

## Valeurs fermées

Libellé affiché = libellé relu. La valeur du milieu est celle du JSON, et celle enregistrée.

### Budget

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Moins de 250 000 € | moins_250 | Moins de 250 000 € |
| 250 000 à 350 000 € | de_250_a_350 | 250 000 à 350 000 € |
| 350 000 à 450 000 € | de_350_a_450 | 350 000 à 450 000 € |
| 450 000 à 600 000 € | de_450_a_600 | 450 000 à 600 000 € |
| Plus de 600 000 € | plus_600 | Plus de 600 000 € |
| Ne sait pas | ne_sait_pas | Ne sait pas |
| Autre | autre | Autre |

### Type de projet

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Maison et terrain | maison_terrain | Maison et terrain |
| Maison seule | maison_seule | Maison seule |
| Terrain seul | terrain_seul | Terrain seul |
| Rénovation | renovation | Rénovation |
| Autre | autre | Autre |

### Foyer

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Une personne | une_personne | Une personne |
| Couple | couple | Couple |
| Famille | famille | Famille |
| Autre | autre | Autre |

### Chambres

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 3 | 3 |
| 4 | 4 | 4 |
| 5 ou plus | 5_ou_plus | 5 ou plus |
| Ne sait pas | ne_sait_pas | Ne sait pas |
| Autre | autre | Autre |

### Horizon

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Dès que possible | des_que_possible | Dès que possible |
| Moins de 6 mois | moins_6_mois | Moins de 6 mois |
| 6 à 12 mois | six_a_douze_mois | 6 à 12 mois |
| 1 à 2 ans | un_a_deux_ans | 1 à 2 ans |
| Plus de 2 ans | plus_de_deux_ans | Plus de 2 ans |
| Ne sait pas | ne_sait_pas | Ne sait pas |
| Autre | autre | Autre |

### Terrain possédé

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Oui | oui | Oui |
| Non | non | Non |
| En recherche | en_recherche | En recherche |
| Ne sait pas | ne_sait_pas | Ne sait pas |
| Autre | autre | Autre |

### Mode de contact

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| E-mail | email | E-mail |
| Téléphone | telephone | Téléphone |
| E-mail et téléphone | les_deux | E-mail et téléphone |
| Ne sait pas | ne_sait_pas | Ne sait pas |
| Autre | autre | Autre |

### Fin de conversation

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Validée | validee | Validée |
| Clôturée | cloturee | Clôturée |
| Abandonnée | abandonnee | Abandonnée |
| Autre | autre | Autre |

### Niveau

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Niveau 1 | 1 | Niveau 1 |
| Niveau 2 | 2 | Niveau 2 |
| Niveau 3 | 3 | Niveau 3 |
| Autre | autre | Autre |

### Maturité (`analyse.maturite`)

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Découverte | decouverte | Découverte |
| Réflexion | reflexion | Réflexion |
| Prêt à avancer | pret_a_avancer | Prêt à avancer |
| Autre | autre | Autre |

### Format (`suivi.format`)

| Libellé affiché | Valeur JSON | Libellé relu |
|---|---|---|
| Bannière | banniere | Bannière |
| E-mail | email | E-mail |
| Réseaux | reseaux | Réseaux |
| Bouche à oreille | bouche_a_oreille | Bouche à oreille |
| Autre | autre | Autre |

## Suivi côté Hectarion (pas dans le JSON)

Valeur postée par le formulaire de la fiche. Colonne `statut_traitement`.

| Libellé affiché | Valeur postée | Libellé relu |
|---|---|---|
| Nouveau | nouveau | Nouveau |
| En cours | en_cours | En cours |
| Traité | traite | Traité |
| Sans suite | sans_suite | Sans suite |

La note interne est un texte libre, 5000 caractères, hors contrat d’échange.

## Refus

Pièce absente, JSON invalide, version inconnue, signature fausse, date hors fenêtre, identifiant déjà vu, schéma ou longueur : aucune fiche. Une ligne de quarantaine garde l’identifiant du message, la date de réception, l’expéditeur, le sujet et le motif. Pas le corps, pas la pièce.
