# Biome Constructions — site vitrine + back-office

Site public de **Biome Constructions** (constructeur de maisons en Wallonie) et
son interface d'administration. Le site est **en production** sur
<https://www.biome.immo>.

Le front public reproduit une maquette conçue dans Claude Design ; le
back-office `/admin` permet à l'équipe de gérer les contenus éditables et de
consulter les demandes reçues.

**Rendu desktop (`>1024px`) = référence pixel-perfect** vis-à-vis de la maquette.
Le responsive mobile (`≤480px`) et tablette (`481–1024px`) est une adaptation
distincte, validée par le client — la maquette source est desktop-only. Détail :
[`docs/mobile-strategie.md`](docs/mobile-strategie.md).

---

## Sommaire

- [Stack](#stack)
- [Démarrage local](#démarrage-local)
- [Où se trouve quoi](#où-se-trouve-quoi)
- [Les trois sources de données](#les-trois-sources-de-données)
- [Back-office : ce qui est éditable](#back-office--ce-qui-est-éditable)
- [Rôles et permissions](#rôles-et-permissions)
- [Flux sortants](#flux-sortants)
- [E-mails](#e-mails)
- [Routes publiques](#routes-publiques)
- [Commandes et tâches planifiées](#commandes-et-tâches-planifiées)
- [Variables d'environnement](#variables-denvironnement)
- [Tests](#tests)
- [Déploiement](#déploiement)
- [Conventions](#conventions)
- [Documentation détaillée](#documentation-détaillée)

---

## Stack

| Couche | Choix |
|---|---|
| Framework | Laravel 12 (PHP 8.2+ ; **8.3 en production**) |
| Back-office | Filament 5.6 (panneau `/admin`), Livewire 4 |
| Permissions | `spatie/laravel-permission` |
| Base de données | MySQL |
| Front public | Blade + CSS/JS **statiques** servis depuis `public/` |
| E-mails | API HTTP Mailjet (transport maison) |
| Hébergement | Mutualisé OVH |

> **Aucune étape de build front.** Il n'y a ni npm, ni Vite, ni Tailwind, ni
> `node_modules` : les CSS/JS sont écrits directement dans `public/css` et
> `public/js`. C'est un choix imposé par l'hébergement mutualisé — voir
> [Déploiement](#déploiement).

---

## Démarrage local

```bash
composer install
cp .env.example .env
php artisan key:generate
# renseigner DB_* dans .env (base par defaut : biome_site)
php artisan migrate --seed
php artisan serve
```

- Site public : <http://localhost:8000>
- Back-office : <http://localhost:8000/admin>

Le seeder crée les rôles, les permissions et des contenus de démarrage.

---

## Où se trouve quoi

```
app/
  Console/Commands/        Commandes artisan du projet (voir plus bas)
  Filament/
    Actions/               Actions de tableau partagees (ex. BasculerTraiteAction)
    Resources/             17 ressources = les ecrans du back-office
    Resources/Concerns/    Traits de gating par section de permission
  Http/Controllers/        Pages publiques + reception des formulaires (Leads/)
  Mail/                    E-mails + transport Mailjet maison
  Models/                  Modeles Eloquent (1 classe = 1 table)
  Models/Concerns/         Traits partages entre modeles
  Permissions/             Matrice des permissions par section
  Services/
    Crm/                   Acces aux biens (LECTURE) + transmission des leads (ECRITURE)
    Formulaires/           Constructeur de formulaires : validation, versioning
    Images/                Optimisation/vignettes
    Rh/                    Transmission des candidatures a la plateforme de recrutement
  Support/                 Helpers (MediaSite, NotificateurLead, NomBien...)
resources/
  views/pages/             Une vue par page publique
  views/components/        Composants Blade partages (header, footer, modales...)
  views/formulaires/       Rendu public des formulaires personnalises
  data/                    Donnees statiques (communes de Wallonie)
public/
  css/  js/                Feuilles de style et scripts servis tels quels
  css/tokens.css           Tokens de design (couleurs, polices, espacements)
  css/mobile.css           TOUT le responsive (voir Conventions)
  images/ fonts/           Assets
database/migrations/       Historique du schema (la base "vit" dans Git)
config/site.php            Coordonnees, GPS du siege, listes metiers/zones
docs/                      Runbooks detailles
maquettes/                 Export Claude Design (reference visuelle, non servi)
CLAUDE.md                  Regles du projet — fait autorite
```

---

## Les trois sources de données

C'est le point le plus important pour comprendre le projet.

### 1. Les biens à vendre — CRM, en LECTURE SEULE

Les biens et **surtout leurs photos** proviennent du CRM interne (« Portail
vendeur »). Ils ne sont **jamais éditables** dans l'admin du site.

Le CRM **pousse** les biens vers le site (modèle PUSH, endpoints `/api/crm/*`
protégés par jeton + signature HMAC), qui les stocke dans un **miroir local**
(`biens_miroir`). Le point d'accès unique est
`App\Services\Crm\CrmClientInterface`, avec deux implémentations :

| `CRM_SOURCE` | Implémentation | Usage |
|---|---|---|
| `miroir` | `MiroirCrmClient` | **Production** — lit le miroir alimenté par le CRM |
| `fixtures` | `FixtureCrmClient` | Développement local, sans CRM |

Les deux sont enveloppées dans `CrmClientEnCache` (TTL `CRM_CACHE_TTL`, 900 s).

> ⚠️ **Aucune écriture des données de biens vers le CRM.** L'interdiction porte
> sur les fiches et les photos — la transmission des leads, elle, est un flux
> sortant légitime (voir [Flux sortants](#flux-sortants)).

### 2. Les contenus éditables — MySQL

Actualités, offres d'emploi, partenaires, réalisations, carrousel d'accueil et
contenus de pages statiques : gérés dans l'admin, stockés en base.

### 3. Les demandes reçues — MySQL

Tous les formulaires publics écrivent en base et sont consultables dans l'admin.
**Le site est toujours la source de secours** : une demande est enregistrée
localement avant toute tentative de transmission externe.

---

## Back-office : ce qui est éditable

**Contenu du site** — Contenus de page, Carrousel accueil, Actualités, Offres
d'emploi, Réalisations, Partenaires.

**Formulaires reçus** — Candidatures, Candidatures partenaires, Demandes d'info
bien, Demandes de visite, Leads terrain, Messages de contact, Téléchargements du
guide.

**Formulaires personnalisés** — un constructeur permet de créer des formulaires
sur mesure (16 types de champs, logique conditionnelle, versioning) et d'en
consulter les réponses.

**Administration** — Utilisateurs, Rôles.

> Les fiches de demandes sont en **consultation seule**, à une exception près :
> les **leads terrain** sont éditables (correction/complétion par l'équipe).

---

## Rôles et permissions

Quatre rôles : **admin**, **utilisateur**, **invite**, **terrain**.

Les droits sont découpés par **section**, avec les actions `view` / `create` /
`update` / `delete` :

```
actualites  offres-emploi  partenaires  realisations  slides-accueil
contenus-pages  formulaires  leads-terrain  formulaires-builder
reponses-formulaires  utilisateurs
```

Le rôle **terrain** est volontairement limité : il n'a que `leads-terrain.view`
et `leads-terrain.update`, pour une personne qui ne traite que les demandes de
terrain.

Les ressources Filament sont protégées par le trait
`App\Filament\Resources\Concerns\VerifiePermissionsSection`. **Toute action
personnalisée doit vérifier sa permission explicitement** — l'autorisation
automatique de Filament ne les couvre pas.

---

## Flux sortants

Deux flux poussent des données vers l'extérieur. **Aucun n'est exécuté dans la
requête du visiteur** : l'hébergement n'a pas de worker de file d'attente, et une
panne externe ne doit jamais faire échouer une soumission.

### Leads acheteurs → CRM

Demandes de visite, demandes d'info sur un bien, messages de contact « maison »
et soumissions de formulaires personnalisés marqués comme tels.

`App\Services\Crm\TransmetteurLeadCrmInterface` — deux transports :
`journal` (bouchon, par défaut) ou `http` (envoi réel, Bearer + HMAC).

### Candidatures → plateforme de recrutement

`App\Services\Rh\TransmetteurCandidatureRhInterface` — même principe
(`RH_TRANSPORT` : `journal` ou `http`). Envoi multipart (CV + lettre), respect du
débit imposé par la plateforme, réessais plafonnés, et suivi visible dans l'admin
(colonne « Recrutement »).

Dans les deux cas : les échecs **définitifs** (charge utile refusée, jeton
invalide) sortent de la file et sont signalés, plutôt que réessayés en vain.

---

## E-mails

L'hébergement mutualisé OVH **bloque le SMTP sortant**, y compris vers les
serveurs mail d'OVH. Les e-mails partent donc via l'**API HTTP de Mailjet**,
avec un transport maison (`App\Mail\Transport\MailjetApiTransport`), enregistré
sous le mailer `mailjet`.

Concernés : notifications de leads à l'équipe, invitations et réinitialisations
de mot de passe des comptes admin, accusés de réception des formulaires.

Diagnostic : `php artisan mail:test <adresse>` affiche la configuration et
l'erreur exacte.

> ⚠️ Les sorties réseau **ne fonctionnent pas depuis le shell SSH** sur cet
> hébergement, mais fonctionnent depuis PHP en contexte web. Un test `curl` en
> SSH donnera un faux négatif.

---

## Routes publiques

| Page | Route |
|---|---|
| Accueil | `/` |
| Acheter (liste des biens) | `/acheter` |
| Fiche d'un bien | `/biens/{id}` |
| Redirection canonique CRM (QR codes des affiches vitrine) | `/bien/{type}/{id}` |
| Vendre mon terrain | `/vendre-mon-terrain` |
| Investir | `/investir` |
| Qui sommes-nous | `/qui-sommes-nous` |
| Partenaires & sous-traitants | `/partenaires` |
| Questionnaire metier d'un sous-traitant (lien nominatif) | `/partenaires/questionnaire/{metier}/{uuid}` |
| Actualités | `/actualites` |
| Offres d'emploi | `/offres-emploi` |
| Détail d'une offre | `/offres-emploi/{slug}` |
| Contact | `/contact` |
| Formulaire personnalisé | `/formulaires/{slug}` |

> La candidature partenaire (`/partenaires`) est un **formulaire de
> qualification en 4 étapes** : métiers et effectif → zone et disponibilité →
> collaboration → identité, suivi d'une étape **facultative** de photos de
> réalisations. Il reprend le périmètre de l'ancien plugin WordPress
> `biome-partenaires`. Détail des choix :
> [`docs/architecture-donnees.md`](docs/architecture-donnees.md).

> Les **questionnaires métier** (électricien, chauffage/sanitaire) collectent
> ensuite les conditions de collaboration et la grille de prix. Ils ne sont pas
> publics : l'équipe envoie au sous-traitant le lien contenant son `uuid`,
> obtenu dans l'admin par l'action « Lien questionnaire ». La page est en
> `noindex`, pré-remplie de ses réponses connues, et sa structure se modifie
> dans [`config/questionnaires-metier.php`](config/questionnaires-metier.php)
> sans migration.

> `/bien/{type}/{id}` (au singulier) est une **redirection 302** vers la fiche,
> construite depuis la clé CRM `type/id`. Elle existe pour les **QR codes des
> affiches vitrine imprimées par les agences** : la clé CRM ne change jamais, donc
> un QR code imprimé reste valide même si le schéma d'URL du site évolue. Un bien
> inconnu ou plus visible (retiré, vendu au-delà du délai d'affichage) renvoie un
> **404 franc**, jamais l'accueil. Contrat détaillé :
> [`docs/contrat-crm-reponse-site.md`](docs/contrat-crm-reponse-site.md), Round 5.

Endpoints machine (jamais appelés par un navigateur) :

| Endpoint | Rôle |
|---|---|
| `POST /api/crm/biens` | Le CRM crée/met à jour un bien |
| `POST /api/crm/biens/resynchronisation` | Resynchronisation complète du miroir |
| `POST /api/crm/biens/{cle}/medias` | Envoi d'une photo ou d'un PDF |
| `GET /planificateur/{jeton}` | Déclenche le scheduler (cron HTTP externe) |

Les trois endpoints `/api/crm/*` sont protégés par jeton **et** signature HMAC.

---

## Commandes et tâches planifiées

| Commande | Rôle |
|---|---|
| `app:purger-leads-anciens` | Purge RGPD des demandes au-delà de la durée de conservation |
| `app:transmettre-leads-crm` | Transmet les leads acheteurs au CRM |
| `app:transmettre-candidatures-rh` | Transmet les candidatures à la plateforme de recrutement |
| `app:verifier-bce-partenaires` | Vérifie les numéros d'entreprise des candidatures partenaires auprès du portail BCE |
| `app:importer-partenaires-wordpress` | Reprend les candidatures partenaires de l'ancien site WordPress depuis un dump SQL (manuel, idempotent) |
| `biens:regenerer-vignettes` | Régénère les vignettes web des photos de biens |
| `crm:inspecter-bien` | Inspecte un bien du miroir (diagnostic) |
| `mail:test` | Teste l'envoi d'e-mail et affiche l'erreur exacte |

**Planification** (`routes/console.php`) : purge quotidienne, transmissions
toutes les 15 minutes, vérification BCE toutes les heures. La transmission RH
n'est planifiée que si `RH_TRANSPORT=http`, et la vérification BCE que si
`BCE_TRANSPORT=http` — en mode bouchon, chaque passage consommerait une
tentative pour rien.

> Le mutualisé OVH n'expose pas de cron système utilisable : le scheduler est
> déclenché par un **cron HTTP externe** appelant `/planificateur/{jeton}`.

---

## Variables d'environnement

Seul `.env.example` est versionné. **Aucun secret dans le dépôt.**

| Variable | Rôle |
|---|---|
| `CRM_SOURCE` | `miroir` (prod) ou `fixtures` (local) |
| `CRM_CACHE_TTL` | Durée du cache des biens (900 s) |
| `CRM_INGEST_TOKEN` / `CRM_INGEST_HMAC_SECRET` | Émis **par le site** pour que le CRM pousse les biens |
| `CRM_LEADS_TRANSPORT` / `CRM_LEADS_URL` / `CRM_LEADS_TOKEN` / `CRM_LEADS_HMAC_SECRET` | Transmission des leads vers le CRM |
| `RH_TRANSPORT` / `RH_INGEST_URL` / `RH_INGEST_TOKEN` / `RH_INGEST_HMAC_SECRET` | Transmission des candidatures |
| `BCE_TRANSPORT` / `BCE_API_KEY` | Vérification BCE des candidatures partenaires (portail Biome, **pas** la BCE officielle) |
| `RH_EXTERNAL_ID_PREFIX` | Préfixe de test (campagne d'essais uniquement) |
| `MAIL_MAILER` | `mailjet` en production, `log` en local |
| `MAILJET_APIKEY` / `MAILJET_APISECRET` | Identifiants Mailjet |
| `MAIL_RECEPTION_LEADS` | Adresse interne recevant les notifications |
| `RETENTION_LEADS_MOIS` | Durée de conservation RGPD (24) |
| `CRON_TOKEN` | Protège l'endpoint du planificateur |
| `SITE_NOINDEX` | Bloque l'indexation (préproduction) |

---

## Tests

```bash
php artisan test
```

**677 tests / 4249 assertions.** Ils couvrent les routes publiques, les CRUD de
l'admin, la validation des formulaires, les permissions par rôle, les flux
sortants (CRM et RH), le socle responsive et la purge RGPD.

Style de code : `vendor/bin/pint --test` (vérification seule). Le projet
**n'applique pas** Pint automatiquement — des écarts de style préexistants
subsistent volontairement, pour ne pas noyer l'historique Git dans des
changements cosmétiques.

---

## Déploiement

Hébergement **mutualisé OVH**, avec des contraintes fortes :

- **Pas de process persistant** : aucun worker de queue en démon.
- **`php artisan config:cache` est INTERDIT** — il provoque une erreur 500.
- **PHP CLI hors PATH** : utiliser `/usr/local/php8.3/bin/php`.
- Le dossier personnel **est** la racine Laravel ; la racine web pointe sur
  `public/`.
- Les assets JS de Livewire doivent être servis en statique
  (`public/vendor/livewire`), sinon l'admin ne se connecte pas.

Séquence de mise à jour :

```bash
cd ~ && git pull
/usr/local/php8.3/bin/php artisan migrate --force
/usr/local/php8.3/bin/php artisan optimize:clear
```

Détail complet et pièges rencontrés :
[`docs/deploiement-ovh.md`](docs/deploiement-ovh.md) et
[`docs/deploiement-preproduction.md`](docs/deploiement-preproduction.md).

---

## Conventions

- **Commentaires et noms internes en français SANS accents** (les textes
  affichés aux visiteurs gardent leurs accents).
- **Aucune requête SQL concaténée** : Eloquent / Query Builder uniquement.
- **Aucun secret dans le code** : tout dans `.env`.
- PSR-12, indentation 4 espaces, typage strict autant que possible.
- **Responsive** : tout le CSS mobile vit dans `public/css/mobile.css`, et
  uniquement dans des media queries `max-width:1024px`, `max-width:480px` ou
  `min-width:1025px`. Cette règle est **vérifiée par un test**
  (`MobileSocleTest`) — le desktop ne doit jamais être affecté.
- **Zones à ne pas modifier** : `vendor/`, les tokens de design, le `.env` réel.

Ces règles font autorité et sont détaillées dans
[`CLAUDE.md`](CLAUDE.md).

---

## Documentation détaillée

| Document | Sujet |
|---|---|
| [`architecture-donnees.md`](docs/architecture-donnees.md) | Qui possède quelle donnée, section par section |
| [`contrat-crm-champs.md`](docs/contrat-crm-champs.md) | Champs échangés avec le CRM |
| [`contrat-crm-reponse-site.md`](docs/contrat-crm-reponse-site.md) | Contrat d'ingestion des biens |
| [`deploiement-ovh.md`](docs/deploiement-ovh.md) | Contraintes de l'hébergement |
| [`deploiement-preproduction.md`](docs/deploiement-preproduction.md) | Pièges réels rencontrés en déploiement |
| [`mobile-strategie.md`](docs/mobile-strategie.md) | Stratégie responsive et méthode de recette |
| [`design-tokens.md`](docs/design-tokens.md) | Tokens extraits de la maquette |
| [`frontend-composants.md`](docs/frontend-composants.md) | Composants Blade partagés |
| [`rgpd.md`](docs/rgpd.md) | Données personnelles, consentements, purge |
| [`mailjet-invitations.md`](docs/mailjet-invitations.md) | E-mails et comptes admin |
| [`contenu-editorial.md`](docs/contenu-editorial.md) | Contenus rédactionnels |
