# Reponse Site — contrat de donnees site vitrine <-> CRM Portail vendeur

**De :** equipe Site vitrine Biome
**A :** equipe CRM Biome (Portail vendeur)
**Date :** 2026-07-03
**En reponse a :** votre `reponse-crm-site-contrat-champs.md`

Merci pour cette reponse detaillee. Reponses point par point ci-dessous. En resume :
**nous acceptons le modele PUSH + miroir** (section 0), **nous acceptons la cle `type:id`**
(votre point n°1), et nous vous proposons ci-dessous le contrat d'ingestion cote site
(endpoints, auth, medias). Les points marques 🟡 attendent un arbitrage de Laurent /
la direction Biome.

---

## 0. PUSH + miroir : ACCEPTE

Le modele PUSH est meme preferable pour nous : le site tourne sur un mutualise OVH,
un miroir local le rend auto-porteur (pas de dependance reseau au CRM pour afficher
une page, pas de probleme de latence/quota, SEO stable).

**Ce que ca change cote site (nous nous en chargeons) :**
- Nouvelles tables MySQL : `biens_miroir` (+ photos/documents associes), **strictement
  NON editables dans l'admin** (aucune ressource Filament de modification). La regle
  CLAUDE.md « aucune duplication editable des biens » est preservee : le miroir est en
  ecriture unique par le CRM, le CRM reste la source de verite.
- Notre interface interne `CrmClientInterface` (qui alimente deja toutes les pages)
  reste inchangee : son implementation lira le miroir local au lieu des fixtures
  actuelles. Aucune page ne change.
- Le cache 15 minutes devient caduc, comme vous le dites.
- Les fixtures actuelles (ach-1...ach-10) restent uniquement pour les tests automatises
  et la recette locale, jusqu'a la bascule.

### 0.1 Contrat d'ingestion des biens que nous proposons (cote site)

| Element | Proposition site |
|---|---|
| Endpoint upsert bien | `POST /api/crm/biens` — corps JSON = payload d'UN bien (votre volet 1), upsert par cle `type:id`. Retour `200 { statut: "cree" \| "mis_a_jour" }`. |
| Changement de statut | Inclus dans le meme upsert (champ `statut` : `publie` / `vendu` / `retire`). Pas d'endpoint separe necessaire. |
| Resynchro complete | `POST /api/crm/biens/resynchronisation` — tableau de TOUTES les cles `type:id` publiees + payloads ; le site marque `retire` tout bien local absent de la liste. Frequence proposee : 1x/jour la nuit. |
| Medias | `POST /api/crm/biens/{type:id}/medias` — **multipart** (fichier + JSON meta : categorie `photo` / `photo_principale` / `pdf_prix` / `plan` / `peb`, ordre, legende optionnelle). Retour l'URL publique servie par le site. `DELETE` par identifiant de media pour les retraits. |
| Auth | **Cle API dediee + HMAC SHA-256 du corps** (en-tete signature), meme schema que votre webhook DocuSign — symetrique a ce que vous proposez pour les leads. Cle de recette distincte de la prod (comme votre §2.5). |
| Limites OVH a connaitre | Taille max d'upload PHP a cadrer ensemble (proposition : 15 Mo/fichier, photos compressees cote CRM avant envoi si besoin). Pas de streaming exotique : du multipart HTTP simple. |

> Nous vous fournirons la spec detaillee (JSON exemple + en-tetes) des que la reunion
> a acte ce cadre.

---

## 1. Volet 1 — reponses sur les champs

**Valeurs numeriques + codes, le site compose les libelles : OUI**, c'est notre
preference confirmee (prix, surfaces, terrain, loyer, rendement, titre investissement).
Inutile de pousser les chaines pre-formatees.

| Point souleve | Reponse site |
|---|---|
| `province` libelle + code | Parfait. Nos codes filtres actuels : `brabant`, `hainaut`, `namur`, `liege`, `luxembourg`. |
| `badge` | **ACTE (Laurent, 2026-07-03) : oui, champ editable cote CRM.** Liste des valeurs : « Nouveau », « Vendu », « Bientot ». Le site affiche le badge tel quel, masque si absent. NB : le badge « Vendu » pourra etre pose automatiquement par le CRM lors du passage au statut vendu (a votre main), le site n'impose rien. |
| `terrain` — semantique | Proposition : pour une **maison/parcelle** = surface de la parcelle (m²) ; pour un **appartement** = surface exterieure (jardin/terrasse/balcon) avec un champ `terrain_type` (code : `parcelle` / `jardin` / `terrasse` / `balcon` / absent) pour que le site adapte le libelle. Si absent : la ligne est masquee. |
| `disponibilite` non exposee (API HFSQL) | OK pour demarrer sans : champ nullable, le filtre « Disponibilite » du site sera masque tant que la donnee n'est pas fiable. A activer quand l'equipe API HFSQL l'expose. |
| `peb` null pour appartements | OK, le site masque la ligne PEB si null. |
| `annee` incertaine | OK nullable, ligne masquee si absente. Enrichissement CRM en secours si la direction y tient. |
| Legende par photo | **OUI, nous en voulons** : la maquette validee affiche une legende par photo dans la galerie du bien (ex. « Sejour lumineux »). Petit ajout CRM justifie. Nullable : le site n'affiche rien si vide. |
| Description mono-bloc | OK : le site conserve les retours a la ligne (nl2br). Pas besoin de multi-blocs. |
| Plans rez/etage | Pas besoin de categorie fine : le site affiche « Les plans » comme une liste de documents avec leur libelle GED + un bouton de telechargement global. La categorie `plan` suffit, le libelle vient de la GED. |
| GPS / carte | **ACTE (Laurent, 2026-07-03) : coordonnees saisies cote CRM** et poussees avec le payload (lat/lng). Pas de geocodage cote site. En attendant la donnee : carte statique, comme aujourd'hui. |
| Arguments « pourquoi cette maison » | **ACTE (Laurent, 2026-07-03) : section conservee, composee depuis des donnees STRUCTUREES** — pas de champ libre a saisir par bien. Le site compose les cartes a partir de : elements fixes cote site (« Maison neuve », « Garantie »), donnees deja au contrat (PEB), champs existants cote CRM a ajouter au payload (**type de chauffage**, **panneaux solaires**), et un champ **cuisine equipee a ajouter cote CRM** (prevu par vos soins). Chaque carte est masquee si la donnee est absente. |
| `chauffage` (nouveau champ payload) | **A ajouter au payload** : type de chauffage — la donnee existe deja cote CRM. Alimente une carte « Pourquoi choisir cette maison ». |
| `panneaux_solaires` (nouveau champ payload) | **A ajouter au payload** : booleen — la donnee existe deja cote CRM. Idem. |
| `cuisine_equipee` (nouveau champ payload) | **A ajouter cote CRM puis au payload** (confirme par Laurent). Idem. |
| Points de proximite « Situation du bien » | **ACTE (Laurent, 2026-07-03) : champ d'enrichissement a creer cote CRM** — liste de points de proximite saisie par bien (« Ecole primaire a 3 min », « Commerces a 5 min »...), poussee avec le payload. Nullable : le site n'affiche que la carte + l'adresse si absent. |
| Biens similaires | **Calcul cote site, accepte** (meme commune / type / fourchette de prix ±20 %) — trivial une fois le miroir en place. |
| Biens vendus/retires | **ACTE (Laurent, 2026-07-03) : conserve puis desaffiche apres un delai.** Le bien passe « Vendu » : il disparait des listes/filtres, sa fiche reste accessible avec un bandeau « Vendu » pendant un delai configurable cote site (proposition : 60 jours, variable d'env), puis la fiche est masquee a son tour. |

### 1.3 Investissement

- Flag « mis en avant investissement » dedie : parfait, c'est exactement ce que la page
  Investir attend.
- `rendement`, `loyer_estime` en valeurs numeriques : OK, le site compose l'affichage.
- `type_investissement` : **ACTE (Laurent, 2026-07-03) : champ CRM saisi** (enum courte
  Appartement / Studio / Duplex), pas de derivation HFSQL.
- `titre_investissement` : OK pour composer cote site a partir de type + chambres.

---

## 2. Volet 2 — leads : ACCEPTE, avec details

Votre contrat d'ingestion (cle API + HMAC + idempotence sur `id_local` + accuse
`{ id_crm, statut }`) correspond exactement a ce que notre mecanique attend
(retentatives max 5, marquage transmis). Nous implementons des reception du contrat.

| Point | Reponse site |
|---|---|
| 🔴 `bien_id` = cle `type:id` | **ACCEPTE.** Puisque vous nous poussez la cle canonique avec chaque bien, nous la stockons et la renvoyons telle quelle dans les leads. Aucune table de correspondance. (Transition : tant que le push n'est pas branche, nos leads partent avec les ids fixtures — a ignorer cote CRM ou a filtrer sur l'environnement de recette.) |
| `budget` fourchette texte | Exact, notre formulaire propose des fourchettes (« 300.000 € – 350.000 € »). Proposition : nous envoyons `budget_min` + `budget_max` **numeriques** (bornes de la fourchette, null si « Plus de 500.000 € » pour le max) + le libelle brut dans `remarques`. Dites-nous si ce decoupage vous va. |
| `type_demande` + `souhaits[]` | Phase 1 dans `remarques` : OK pour nous. Si le tri fin devient utile aux vendeurs, on passera en colonne structuree — a votre main. |
| `info_bien` souhait visite sans creneau | Comportement decrit (prospect + lien bien + consigne, pas de visite creee) : OK. |
| Attribution interne | Parfait, rien a specifier cote site. |
| Echecs repetes / notification | Une notification interne cote CRM en cas d'echecs repetes nous convient. Cote site, au-dela de 5 tentatives le lead reste visible dans l'admin avec son statut « En attente » — une relance manuelle pourra etre ajoutee si necessaire. |
| Environnement de test | Tres bien : cle API de recette distincte. Nous ferons de meme pour notre endpoint d'ingestion des biens. |

---

## 3. Recapitulatif des decisions

**Actees cote site (cette reponse) :**
1. PUSH + miroir : accepte, contrat d'ingestion propose en §0.1.
2. `bien_id` = cle `type:id` : accepte.
3. Valeurs numeriques + codes, libelles composes cote site : accepte.
4. Legendes photos : demandees (la maquette les affiche).
5. Biens similaires : calcules cote site.
6. Budget leads : proposition min/max numeriques + libelle en remarques.

**Actees par Laurent (2026-07-03) :**
1. Badge editorial des biens : OUI, champ CRM — valeurs « Nouveau », « Vendu »,
   « Bientot ».
2. Biens vendus : conserves avec bandeau « Vendu » puis desaffiches apres un delai
   configurable cote site (proposition 60 jours).
3. `type_investissement` : champ CRM saisi.
4. GPS : coordonnees saisies cote CRM, poussees avec le payload.
5. Section « Pourquoi choisir cette maison » : composee depuis des donnees
   structurees. **Cote site : desormais CABLE (fiche bien dynamique par bien).**
   Cartes universelles (Construction neuve, PEB reel, Garantie) toujours
   affichees ; cartes d'equipement affichees UNIQUEMENT si le bien les possede :
   `chauffage_sol`, `panneaux_photovoltaiques`, `cuisine_equipee` (bool sous
   `technique.*` du payload, deja acceptes a l'ingestion et exposes par
   MiroirCrmClient). Tant que le CRM ne les pousse pas, ces cartes ne
   s'affichent simplement pas (aucune fausse allegation). Dependance : le CRM
   doit peupler ces champs par bien.
6. Points de proximite « Situation du bien » : champ d'enrichissement a creer cote CRM
   (liste saisie par bien, nullable).

**Plus aucun point en attente cote site : le document est complet.**

**Prochaines etapes proposees :**
1. Reunion commune pour acter §0.1 (endpoints site) et les ajouts de champs CRM
   (badge, legendes photos, cuisine_equipee, points de proximite, GPS,
   type_investissement, investissement).
2. Le CRM fournit l'exemple de payload bien (JSON) + le contrat leads definitif.
3. Cote site : implementation du miroir + endpoints d'ingestion + bascule de
   l'implementation `CrmClientInterface` (fixtures -> miroir) + branchement du
   transmetteur de leads HTTP. Les tests automatises existants (305) couvrent deja
   les deux mecanismes via des bouchons : la bascule est a faible risque.

---

## Round 2 — reponses aux questions d'implementation (2026-07-08)

**De :** equipe Site vitrine Biome — **A :** equipe CRM (Portail vendeur)
**En reponse a :** vos questions d'implementation sur l'ingestion des biens et des medias.

Rappel : les 3 endpoints que vous confirmez sont ceux que nous avons proposes en 0.1,
nous sommes donc alignes. Reponses point par point. Marqueurs : 🟢 confirme cote site,
🟡 a la main de Laurent, 🔴 spec qu'il nous faut de votre cote pour implementer.

### 1. Endpoints
🟢 Confirmes : `POST /api/crm/biens`, `POST /api/crm/biens/resynchronisation`,
`POST /api/crm/biens/{type:id}/medias`. Identiques a notre 0.1. Pas encore implementes
(le site tourne sur fixtures + bouchon) ; implementation des que les points 🔴 sont figes.
🔴 Nous acceptons `type:id` comme segment d'URL unique (ex. `maison:123`) ; confirmez que
l'`id` ne contient jamais de `/`. 🟡 Delai : annonce par Laurent.

### 2. Identifiants (URL base + cle API + secret HMAC, recette + prod)
🟡 Le site etant le RECEPTEUR, c'est nous qui generons et vous transmettons, en 2 jeux :
- URL de base : recette = `https://new.biome.immo` ; prod = a confirmer par Laurent
  (`biome.immo` / `www.biome.immo`).
- token Bearer + secret HMAC par environnement.
Secrets generes par Laurent, transmis par canal securise (jamais dans le code ni un depot).
Cote site ils vivent en variables d'environnement (`.env`, non versionne).

### 3. Auth : Bearer + X-Signature
🟢 Schema accepte. 🔴 A figer pour un match octet par octet :
- Biens : HMAC-SHA256 sur le CORPS BRUT (bytes recus avant tout parsing), sortie base64
  dans `X-Signature`. Confirmez : base64 standard (non url-safe) ; presence ou non d'un
  horodatage/nonce anti-rejeu inclus dans la signature (recommande), sinon Bearer + HTTPS
  consideres suffisants.
- Medias (multipart) : envoyez la construction EXACTE de la chaine canonique signee (liste
  ordonnee des metadonnees, separateur, encodage, representation du sha256 : hex minuscule
  ou base64) + UN EXEMPLE TRAVAILLE (entrees -> chaine canonique -> signature attendue).
  Nous en ferons un test de non-regression garantissant l'egalite.

### 4. Upsert idempotent par cle type:id
🟢 Confirme. Miroir clefe sur (type, id) ; un rejeu du meme type:id met a jour en place,
sans doublon. Retour `201` a la creation, `200` a la mise a jour.

### 5. Elagage des medias
🟢 Confirme : a chaque upsert de bien, `medias.photos[]` fait autorite ; nous supprimons
fichiers + lignes des medias dont la reference n'y figure plus. 🔴 Precisez la CLE DE
REFERENCE d'un media dans `medias.photos[]` (sha256 du fichier ? identifiant media cote
CRM ? nom/URL renvoye par l'endpoint medias) afin qu'elle corresponde de maniere fiable au
fichier uploade. Nous tolerons un manifeste citant un media pas encore uploade (affichage
de ce qui est disponible, sans erreur).

### 6. Resynchronisation nocturne
🟢 Confirme : tout `type:id` present dans le miroir mais absent de la liste autoritaire
passe `retire`. 🔴 La resync transmet-elle les PAYLOADS COMPLETS (elle vaut alors upsert en
masse + elagage) ou SEULEMENT les cles type:id (elagage seul) ? Nous implementons selon
votre choix.

### 7. Medias : taille, sha256, legende
🟢 15 Mo/fichier : accepte cote application. Nous validons le sha256 recu (rejet si ecart)
et stockons/affichons `medias.photos[].legende` (nullable). 🟡 Reserve hebergement : le
mutualise OVH plafonne `upload_max_filesize` / `post_max_size` ; nous cadrons a 15 Mo via
`.user.ini` et verifions sur l'hebergement (si le plafond reel est plus bas, on vous le
signale : compression cote CRM en secours, comme en 0.1). 🔴 Confirmez la liste des types
MIME acceptes par categorie : `photo` / `photo_principale` = jpeg/png/webp ? `pdf_prix` /
`plan` / `peb` = application/pdf ?

### 8. Biens vendus
🟢 Confirme (deja acte par Laurent) : le bien `vendu` disparait des listes/filtres ; sa
fiche reste accessible avec un bandeau « Vendu » pendant un delai configurable cote site
(60 jours par defaut, variable d'env), puis la fiche est masquee. 🔴 Preference : poussez
`statut=vendu` (dans l'upsert) plutot qu'un simple retrait de la resync, pour que nous
affichions le bandeau. Le compteur des 60 jours est pilote COTE SITE (demarre a la premiere
reception du statut vendu) : pas besoin de re-pousser le bien ensuite. Confirmez.

### 9. Codes retour
🟢 200/201 = accepte ; 401 = auth/signature invalide. 🔴 Pour eviter les boucles de rejeu,
nous proposons : rejouer UNIQUEMENT 429 et 5xx (transitoire) ; traiter 400 / 422 comme
PERMANENT (payload / sha256 / signature invalides -> log + alerte, pas de rejeu infini).
Vous confirmez cette semantique ?

### Recapitulatif — ce qu'il nous faut de votre cote (bloque l'implementation)
1. Exemple de payload bien complet en JSON (avec `medias.photos[]`, `statut`, `badge`,
   coordonnees lat/lng, `chauffage`, `panneaux_solaires`, `cuisine_equipee`, points de
   proximite, champs investissement).
2. Spec exacte de la signature des medias (canonicalisation) + un exemple travaille.
3. Reponses aux 🔴 : cle de reference des medias (5), forme de la resync (6), MIME acceptes
   (7), statut vendu (8), semantique de rejeu (9), anti-rejeu biens (3).

### A la main de Laurent
- Delai a communiquer (1) ; domaine de prod exact (2) ; generation et partage des secrets
  recette + prod (nous preparons les emplacements `.env`).

Des reception de ces elements, implementation cote site : tables miroir non editables, 3
endpoints + middleware Bearer/HMAC, bascule `CrmClientInterface` (fixtures -> miroir), puis
branchement du transmetteur de leads. **En attendant, aucun code n'est ecrit (decision
Laurent, 2026-07-08).**

---

## Round 3 — cote site implemente + ecarts de payload a confirmer (2026-07-08)

**De :** equipe Site vitrine Biome — **A :** equipe CRM (Portail vendeur)

Vos specs (round 2 + `contrat-push-biens-site.md` + `contrat-ingestion-leads-site.md`) ont ete
suffisantes : **tout le cote site est implemente, teste et fusionne.** Vos vecteurs de
signature sont reproduits a l'octet pres (test de non-regression). En resume :

- **Ingestion biens** : les 3 endpoints sont livres — `POST /api/crm/biens` (upsert par `type:id`,
  201/200), `POST /api/crm/biens/resynchronisation` (liste de cles -> `retire` les absents),
  `POST /api/crm/biens/{type:id}/medias` (multipart, controle MIME + sha256, elagage par manifeste).
  Auth `Bearer` + `X-Signature` conforme (HMAC du corps brut / de la chaine canonique medias).
- **Miroir + affichage** : les biens pousses alimentent un miroir local (non editable) que les
  pages lisent ; bien vendu masque des listes mais fiche accessible avec bandeau (delai 60 j cote
  site) ; retire / vendu expire masques.
- **Leads** : votre endpoint `POST /api/site/leads` est branche (Bearer + `X-Signature`, `id_local`
  en string, `deja_recu` traite comme succes). On l'active des reception des creds de recette.
- **Codes retour** : on rejoue 429/5xx ; 400/422 traites en permanent. Conforme a votre round 2.

### Mise en service — ce qu'il reste (creds, aucun code)
| Sens | Qui emet les creds | Variables |
|---|---|---|
| Biens (CRM -> site) | **le site** (Laurent) genere et vous transmet | `CRM_INGEST_TOKEN`, `CRM_INGEST_HMAC_SECRET` (recette + prod) |
| Leads (site -> CRM) | **le CRM** nous transmet | `CRM_LEADS_URL`, `CRM_LEADS_TOKEN`, `CRM_LEADS_HMAC_SECRET` (recette + prod) |

URL de base du site : recette = `https://new.biome.immo` ; prod a confirmer. Des qu'on a vos
**URL + jeton + secret de recette leads** et que vous avez nos creds d'ingestion, on lance une
recette croisee (vous poussez le bien `6063`, on pousse un lead de test).

### Ecarts de payload biens (§4) a confirmer / completer de votre cote
Le mapping tourne avec les replis ci-dessous (« masque si absent », valeurs composees cote site) ;
ces points ameliorent la fidelite mais **ne bloquent pas** la mise en service.

| # | Ecart | Repli actuel cote site | Ce qu'on aimerait |
|---|---|---|---|
| 1 | **Code province** absent du payload | derive du `code_postal` (plages Wallonie en dur) | pousser directement le code (`namur`, `liege`, `hainaut`, `brabant`, `luxembourg`) |
| 2 | **Type `parcelle`/`terrain`** | replie sur `maison` (le filtre site ne connait que maison/appartement) | soit aligner la categorie, soit nous confirmer qu'un filtre "terrain" est a ajouter cote site |
| 3 | **Surface de terrain** distincte de l'habitable | une seule `surface_m2` : terrain pour une parcelle, habitable sinon (terrain alors = libelle `type_exterieur` sans m2) -> tri "plus grand terrain" degrade | un champ de surface de terrain distinct (m2) quand il existe |
| 4 | **Disponibilite** (immediate/construction) absente | `null` -> filtre "disponibilite" sans donnee | l'exposer quand l'API HFSQL la fiabilise (deja anticipe) |
| 5 | **Description** du bien absente | `[]` (la fiche compose un texte generique) | un vrai texte descriptif par bien (mono-bloc, nl2br) |
| 6 | **Lot / titre** absent | derive de la partie numerique de l'`id_hfsql` (ex. `parcelle:6063` -> "LOT 6063") | confirmer cette convention, ou pousser un numero de lot / un titre dedie |

Rien d'autre en attente cote site : on est prets pour la recette croisee.

---

## Round 4 — accuse : ecarts round 3 consommes, prets pour la recette (2026-07-09)

**De :** equipe Site vitrine Biome — **A :** equipe CRM (Portail vendeur)

Merci pour votre reponse round 3. **Tous les champs ajoutes/precises sont desormais
consommes cote site** (fusionne). Point par point sur nos 6 ecarts :

| # | Ecart round 3 | Statut cote site |
|---|---|---|
| 1 | Code province | ✅ On GARDE la derivation depuis le `code_postal` (Wallonie), comme convenu. Rien a pousser de votre cote. |
| 2 | Categorie maison/appartement | ✅ On lit **`categorie`** pour le type d'affichage/filtre (les parcelles en `maison`). Plus de derivation depuis `type`. |
| 3 | Surfaces distinctes | ✅ On lit **`technique.surface_habitable`** et **`technique.surface_terrain`** ; le tri "plus grand terrain" utilise `surface_terrain`. `surface_m2` garde en repli habitable. |
| 4 | Disponibilite | ⏳ Toujours `null` cote site, filtre masque tant que l'API HFSQL ne la fiabilise pas (anticipe des deux cotes). |
| 5 | Description | ✅ On lit **`descriptif`** (mono-bloc) : la fiche l'affiche (paragraphes, nl2br) et retombe sur un texte generique s'il est vide. |
| 6 | Lot / titre | ✅ On utilise **`reference`** comme numero de lot (partie numerique), `libelle` en complement. Plus de derivation depuis l'`id`. |

### Reste : echange des creds de recette (seul bloqueur, aucun code)
- **Biens (CRM -> site)** : on vous transmet nos `CRM_INGEST_TOKEN` + `CRM_INGEST_HMAC_SECRET`
  de recette ; base URL recette = `https://new.biome.immo`.
- **Leads (site -> CRM)** : on attend vos `CRM_LEADS_URL` + `CRM_LEADS_TOKEN` +
  `CRM_LEADS_HMAC_SECRET` de recette (deja recus si transmis entre-temps).
- Domaine **prod** a confirmer (`biome.immo` / `www.biome.immo`).

### Recette croisee — on est prets
Des l'echange des creds recette : vous poussez le bien **6063** (avec `categorie`,
`surface_habitable`/`surface_terrain`, `descriptif`, `reference`, `medias.photos[]`), on
verifie l'upsert + les medias + l'affichage (fiche, carte, "Vendu" le cas echeant) ; on
pousse un **lead de test**, on verifie l'accuse `cree`/`deja_recu`. Puis bascule prod
(`CRM_SOURCE=miroir`, `CRM_LEADS_TRANSPORT=http`).

---

## Round 5 — route canonique `type/id` pour les QR codes : ACCEPTEE ET EN LIGNE (2026-08-21)

**De :** equipe Site vitrine Biome — **A :** equipe CRM (Portail vendeur)
**Reference :** votre demande « une route canonique `type/id` vers la fiche d'un bien ».

### Une precision qui change votre diagnostic

Votre demande part de : « votre site route sur ses propres slugs (`ach-3`) ». **C'est
faux en production.** `ach-3` est un identifiant de nos **fixtures de developpement**
(`app/Services/Crm/fixtures/biens.php`), utilisees uniquement en local quand aucun CRM
n'est branche.

En production (`CRM_SOURCE=miroir`), l'identifiant d'URL de la fiche **est deja votre
cle** : `MiroirCrmClient::mapper()` expose `'id' => id_hfsql`. L'URL reelle d'un bien est
donc, aujourd'hui :

```
https://www.biome.immo/biens/parcelle:4521
```

Vous auriez donc pu fabriquer vos QR codes sans nous. **On ajoute quand meme la route
demandee**, pour les deux raisons qui restent valables :

1. **La forme.** Votre argument sur le deux-points est le bon : il est licite (RFC 3986)
   mais mal detecte en pratique, et une affiche imprimee n'est pas rattrapable.
2. **Le decouplage.** Votre URL imprimee ne doit pas dependre de *notre* schema d'URL.
   Aujourd'hui les deux coincident par construction, mais rien ne le garantit demain :
   la route canonique fait de cette coincidence un contrat explicite.

### Reponses a vos 5 questions

**1. Acceptez-vous cette route, sous quelle forme ?** Oui, exactement la forme demandee :

```
GET https://www.biome.immo/bien/{type}/{id}
```

Deux segments, **pas besoin de la variante `parcelle:4521`**. Contraintes de routage :
`{type}` = minuscules alphabetiques, `{id}` = entier. Le type n'est **pas** limite a
`parcelle|appartement` : l'existence est verifiee en base, donc un troisieme type
(`maison` existe deja dans nos donnees) fonctionnera sans livraison de code chez nous.

**2. Bien inconnu ou non publie ?** **404 franc**, verrouille par test — jamais de
redirection vers l'accueil. Sont en 404 : cle inconnue, bien `retire`, et bien `vendu`
au-dela du delai d'affichage. Un bien **vendu recemment** redirige encore, vers sa fiche
portant le bandeau « Vendu » : c'est voulu, une affiche peut etre encore en vitrine.

**3. 301 ou 302 ?** **302** — et c'est un choix delibere contre votre preference.

Un 301 autorise le navigateur a memoriser la correspondance **indefiniment**. Si notre
schema d'URL change un jour, un telephone qui a deja scanne continuerait d'aller vers
l'ancienne adresse : precisement la panne que cette route existe pour eviter. La cible
est **mutable par construction** — c'est sa raison d'etre — donc la redirection n'est pas
permanente. Ces URL ne sont pas destinees a etre indexees, l'argument SEO du 301 ne
s'applique pas. Dites-nous si un 301 vous est necessaire pour une raison qu'on ignore.

**4. D'abord en recette sur `new.biome.immo` ?** **Impossible : cette recette n'existe
plus.** `new.biome.immo` n'etait pas un environnement distinct — c'est **la meme
installation** qui est passee en production sur `www.biome.immo` le 2026-07-16 (decision
client : « on ne reinstalle rien, seul le nom de domaine change »). Il n'y a aujourd'hui
qu'un seul environnement serveur.

Validez donc le scan **directement en production**, ce qui est sans risque ici : la route
est purement additive (elle ne modifie aucune donnee, ne change aucune URL existante) et,
de votre cote, le QR code n'apparait que sur un bien reellement en ligne. Prenez un bien
publie, scannez.

**5. Domaine de prod.** **`www.biome.immo` confirme**, avec le `www`. Ce n'est pas qu'une
convention : l'apex `biome.immo` est redirige en **301 vers `www`** au niveau serveur
(`public/.htaccess`), a l'exception de `/api/` et `/planificateur/`. Mettez donc bien
`www` dans votre configuration — un QR pointant sur l'apex ajouterait un aller-retour
inutile avant l'affichage, sur un reseau mobile.

### Votre configuration

```
SITE_VITRINE_URL_FICHE=https://www.biome.immo/bien/{type}/{id}
```

### Alternative §3 (URL renvoyee dans la reponse au push) : ecartee

Votre analyse est la bonne et on la partage : elle coute une migration chez vous, une
evolution d'API chez nous, **et ne resout pas** le probleme de l'URL perimee apres
impression. Rien ne change dans la reponse a `POST /api/crm/biens`.

### Cote site : ce qui a ete livre

- `GET /bien/{type}/{id}` — `BienController::canonique()`, nommee `bien.canonique`.
- La visibilite est **deleguee a `CrmClientInterface::bien()`**, la meme que la fiche
  ciblee : cette route n'invente aucune regle de publication propre.
- `tests/Feature/BienRouteCanoniqueTest.php` verrouille le contrat annonce ci-dessus
  (redirection, 404 franc, absence de repli sur l'accueil, cas vendu/retire).
