Environnement de développement
Capture des e-mails — Mailpit
En développement, les e-mails ne sont pas envoyés à de vrais destinataires : ils sont interceptés par Mailpit, un serveur SMTP léger doublé d'une interface web. Tout courriel émis par l'application (réinitialisation de mot de passe, notifications, etc.) y est capté et consultable, sans risque de fuite vers l'extérieur.
- Service :
mailpitdedocker-compose.yml, image officielleaxllent/mailpit. Pas de volume : le stockage en mémoire (défaut) suffit en développement, les messages sont éphémères et repartent à zéro à chaque redémarrage du conteneur. - Ports exposés sur l'hôte :
1025— point d'entrée SMTP (réception du courrier).8025— interface web de consultation.
- Interface web : http://localhost:8025 — pour lire les e-mails capturés.
Configuration SMTP
L'application est configurée pour livrer via SMTP (.env / .env.example) :
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=nullMailpit n'exige ni authentification ni chiffrement : MAIL_USERNAME, MAIL_PASSWORD et le chiffrement restent nuls.
Hôte selon le contexte d'exécution (hôte vs conteneur)
Le projet se lance de deux façons, et l'hôte SMTP à viser diffère — exactement comme pour la base de données (voir Base de données & identifiants) :
- Sur l'hôte (
php artisan servelancé directement) : Mailpit est joignable sur127.0.0.1:1025, le défaut de.env. - Dans le conteneur
app:127.0.0.1désigne le conteneur lui-même, pas Mailpit. Il faut viser le nom de service Dockermailpit. Le blocenvironment:du serviceappdansdocker-compose.ymlimpose doncMAIL_HOST=mailpit(avecMAIL_MAILER=smtpetMAIL_PORT=1025), qui prime sur.envpour les commandes en ligne.
Piège (identique à celui des variables DB_*) : php artisan serve — la commande CMD du conteneur — ne transmet à ses workers HTTP qu'une liste blanche restreinte de variables d'environnement (ServeCommand::$passthroughVariables) ; les autres sont relues depuis .env à chaque requête. Sans correctif, les requêtes HTTP servies retomberaient sur 127.0.0.1 au lieu de mailpit. AppServiceProvider::boot() ajoute donc MAIL_MAILER, MAIL_HOST et MAIL_PORT à cette liste blanche, au même titre que les variables DB_* et AWS_ENDPOINT. Régression couverte par tests/Feature/Database/ServePassthroughTest.php.
Démarrer Mailpit
docker compose up -d mailpitPuis ouvrir l'interface : http://localhost:8025.
Stockage objet — MinIO & imgproxy
Le pipeline d'images (originaux sur S3, service via /img/ → imgproxy) est détaillé dans Stockage d'images. Côté environnement de développement :
minio— store S3-compatible local (image officielleminio/minio), ports9000(API) et9001(console web : http://localhost:9001), volumeminiodata. Le root user/password réutiliseAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYdu.env(interpolation compose) — une seule paire d'identifiants.minio-init— one-shotminio/mcqui crée le bucket (AWS_BUCKET, défautroadtrip) idempotamment puis s'arrête :docker compose upsuffit à obtenir un store fonctionnel.imgproxy— port hôte8081(FORWARD_IMGPROXY_PORT), branché surhttp://minio:9000, clés de signatureIMGPROXY_KEY/IMGPROXY_SALTpartagées avec le backend via le même.env.- Piège hôte vs conteneur :
AWS_ENDPOINTvauthttp://localhost:9000dans.env(exécution sur l'hôte) maishttp://minio:9000dans le conteneurapp(imposé pardocker-compose.yml) — même mécanique et même liste blancheservequeDB_*/MAIL_*ci-dessus. - Le routage
/img/en dev relève du proxy Vite du dépôt frontend (comme/api) ; rien à configurer côté backend.
docker compose up -d minio minio-init imgproxyComptes de test (seed)
php artisan db:seed peuple une base de développement représentative du modèle d'identité multi-casquettes (voir Identité & accès). Le DatabaseSeeder enchaîne RoleSeeder (les rôles globaux manager / employee), OrganizationSeeder, OrganizationCsvSeeder, SuperAdminSeeder, DemoScenarioSeeder, ActivationCodeSeeder (qui dote chaque reseller — synthétique, importé ou démo — de ses lots de codes), TagSeeder, ProviderSeeder puis LegacyActivitySeeder.
Le seed superpose trois couches :
- Données de référence réelles — importées d'extraits CSV gitignorés (
OrganizationCsvSeeder,LegacyActivitySeeder) ; - Volume synthétique — généré par factories (
OrganizationSeeder,ProviderSeeder,ActivationCodeSeeder) ; - Casting nommé — des comptes fixes écrits en PHP (
SuperAdminSeeder,DemoScenarioSeeder).
La ligne de partage : une donnée dont la source de vérité vit hors du dépôt passe par un CSV gitignoré ; une donnée inventée est écrite en PHP et existe donc sur tous les checkouts (la CI n'a jamais les CSV — les tests de seed dérivent leurs comptages attendus des fichiers eux-mêmes, zéro en leur absence).
Mot de passe unique : tous les comptes seedés utilisent Pa55w0rd!.
Les e-mails sont déterministes (dérivés du slug d'organisation), donc reproductibles d'un reseed à l'autre — pratique pour des requêtes Bruno ou une connexion rapide. En revanche les noms d'organisation et d'utilisateur sont générés aléatoirement par les factories.
Membres d'organisation
Pour chacun des deux OrganizationType (reseller, region), 10 organisations sont créées (reseller-1 … reseller-10, region-1 … region-10), soit 20 organisations. Chaque organisation est dotée de :
- 1 manager — rôle
manager, statut compteactive, e-mailmanager@<type>-<n>.example.com; - 2 employés — rôle
employee, e-mailsemployee1@<type>-<n>.example.cometemployee2@<type>-<n>.example.com. Leur statut de compte estactivedans ~5 cas sur 6, sinonpending(pour exercer le refus de login des comptes nonactive).
soit 60 comptes membres. L'appartenance (Membership) est toujours active ; le rôle est scopé à l'organisation (tables spatie). Environ un membre sur trois est aussi marqué voyageur (is_traveller).
Exemples :
manager@reseller-1.example.com— manager de l'organisationreseller-1;employee2@region-4.example.com— employé de l'organisationregion-4.
Membres multi-casquettes
Une fois toutes les organisations dotées, 5 membres existants sont rattachés à une seconde organisation (rôle employee également, scopé à cette autre organisation). Ce sont eux qui exercent le modèle « plusieurs casquettes » : un même e-mail appartient alors à deux organisations avec un rôle par organisation. Le tirage étant aléatoire, ces comptes ne sont pas connus à l'avance — les repérer via une requête (User ayant plus d'une organisation) plutôt que par e-mail.
Organisations importées (CSV)
OrganizationCsvSeeder importe les organisations réelles depuis database/regions.csv (Nom,status,contact_email) et database/resellers.csv (Nom,commission,commission_type,status,contact_email). Comme pour le catalogue hérité : données métier réelles, volontairement hors git (règle *.csv de database/.gitignore) — obtenir les fichiers auprès de l'équipe. Le seeder s'ignore avec un avertissement quand un fichier est absent et OrganizationCsvSeederTest se marque skipped ; une valeur de status ou de commission_type inconnue fait échouer le seed bruyamment (::from()). La clé naturelle est (type, nom) : un re-seed est idempotent (par ex. php artisan db:seed --class=OrganizationCsvSeeder après un ajout de lignes).
Chaque organisation importée est dotée du même équipage fictif que les synthétiques (1 manager + 2 employés — le concern partagé StaffsOrganization). Les logins dérivent du slug du nom de l'organisation, jamais du contact_email réel (uniforme même sans e-mail de contact, et le seed ne peut jamais frapper un identifiant sur un domaine réel) : manager@graubunden-ferien.example.com, employee1@graubunden-ferien.example.com, … mot de passe Pa55w0rd!.
Super-admins et compte démo
SuperAdminSeeder crée 3 super-admins : admin1@example.com, admin2@example.com, admin3@example.com (active, reposant uniquement sur le bypass de Gate is_super_admin, voir Identité & accès). À ne pas confondre avec la commande idempotente app:ensure-super-admin, destinée, elle, à l'amorçage hors développement.
admin2 / admin3 restent l'axe pur du modèle : aucune organisation, pas voyageurs. admin1 est enrichi par DemoScenarioSeeder en compte démo omni-casquettes : voyageur, et membre de 4 organisations démo créées par le seeder lui-même (statut approved, admin1 seul membre, clé (type, nom) — donc re-seedable sans doublon) :
| Organisation | Type | Rôle d'admin1 |
|---|---|---|
| Demo Région Léman | region | manager |
| Demo Région Grisons | region | employee |
| Demo Kiosque Gare | reseller | manager |
| Demo Boutique Lac | reseller | employee |
admin1 est aussi client de 3 codes d'activation préfixés DEMO émis par Demo Kiosque Gare : 2 achetés, 1 utilisé, durée 12 h — de quoi dérouler démos et requêtes Bruno sur un état connu d'avance.
Catalogue (prestataires & activités)
Trois seeders complémentaires peuplent le catalogue :
TagSeederétend le registre de tags au-delà du socle seedé par les migrations (affaires à prendre, restrictions, suitabilities — les thèmes, vocabulaire hérité complet, ne sont pas touchés). Idempotent et non destructif (ON CONFLICT DO NOTHINGsur(type, slug)) : il n'écrase jamais un libellé ou une icône édités par un super-admin.ProviderSeedergénère par factories des prestataires fictifs avec leurs activités, pour exercer tout le cycle éditorial : brouillons et origines (imported/signed_up).LegacyActivitySeederimportedatabase/activities.csv, un extrait réel de l'export client (l'ancien système confondait prestataire et activité dans un même enregistrement : chaque ligne CSV donne un prestataire approuvé,origin = imported, plus son activité et ses tags de thème). Les prestataires sont clés parlegacy_pk, donc un re-seed est idempotent ; une étiquette de pays ou une valeur de vocabulaire inconnue fait échouer le seed bruyamment (match non exhaustif) pour signaler un nouvel export à cartographier. Les colonnes sans équivalent dans le nouveau schéma (liens destination, images, champs tour, doublons*_export…) sont ignorées. Le CSV étant une donnée client réelle, volontairement hors git (database/.gitignore), le seeder s'ignore avec un avertissement quand le fichier est absent, et son test de feature se marque skipped — obtenir le fichier auprès de l'équipe pour seeder le catalogue réel.

