Travelise Road-Trip — Backend
Documentation vivante du backend. Ces pages décrivent l'état actuel du projet et évoluent avec lui. Elles ne sont pas un journal de décisions : ce qui n'est pas encore construit est regroupé dans la section Questions ouvertes.
Sommaire
- Modèle de données — fiches métier — une fiche par entité (
app/Models/) décrivant son rôle métier, son cycle de vie et ses relations avec les autres entités - Base de données & identifiants — PostgreSQL, UUIDv7 universels, schéma relationnel généré (mermaid)
- Identité & accès — authentification, organisations (candidature & statut), rôles, super-admin
- Journal d'audit — spatie/laravel-activitylog, capture automatique + entrées explicites, enrichissement par requête
- Environnement de développement — capture des e-mails (Mailpit), stockage objet (MinIO), comptes de test (seed)
- Stockage d'images — disque
s3, MinIO en dev, imgproxy, signature d'URLs (/img/) - Génération de PDF — sidecar Gotenberg, document de code d'activation rendu au clic derrière une URL signée éternelle
- Tests — suite PHPUnit sur PostgreSQL jetable
- Intégration continue — pipeline GitLab
- Déploiement — image FrankenPHP/Octane, compose sur VM derrière nginx
- Observabilité — métriques Prometheus (
GET /api/metrics, FrankenPHP/Caddy, imgproxy), logs JSON vers Loki - Contrat d'API & types partagés — OpenAPI (Scramble) servi sur
GET /api/contract, codegen côté frontend - Base de données — contient le catalogue des entités métier implémentées (
activation_codes,providers,activities, …)
Contexte produit
Le client exploite une activité de voyages-surprises impliquant plusieurs types d'utilisateurs : voyageurs, acteurs touristiques (musées, restaurants, etc.), agences de voyage et revendeurs.
Le projet comporte deux volets :
- Migration de l'interface d'administration — actuellement réalisée sous FileMaker / HubSpot, vers une nouvelle interface web.
- Nouveau produit de road-trips surprise — les clients achètent un road-trip dont les étapes (restaurant, randonnée, musée, etc.) se dévoilent progressivement au fil de la journée. Ce produit prend la forme d'une PWA / application mobile native.
Particularités structurantes :
- Chaque type d'acteur dispose de son propre portail et ne voit que ses propres données (ex. : une agence émet des cartes-cadeaux de road-trip et suit leurs activations). Les types d'acteur actuellement modélisés par l'enum
OrganizationTypesontreseller(revendeur) etregion(région partenaire) ; les agences et les acteurs touristiques ne sont pas encore modélisés. - Le produit mobile doit fonctionner en zones à connectivité faible.
- La mécanique de « révélation » progressive est le cœur du produit et son principal risque technique.
Principes transverses
- REST plutôt que GraphQL — plus simple à mettre en cache, sécuriser et raisonner à ce stade. Le backend est accessible uniquement via REST (aucune vue rendue côté serveur).
- Offline-first — conçu pour la connectivité faible ; les données de la journée sont destinées à être mises en cache sur l'appareil (compromis surprise / hors-ligne assumé : les contenus mis en cache sont présents sur l'appareil mais masqués par la logique applicative).
- Cloisonnement par organisation — isolation multi-portails via une base partagée unique (et non une tenancy multi-bases), pour garder les requêtes inter-acteurs simples.
Questions ouvertes / à cadrer
Éléments anticipés mais non encore implémentés. Ils sortiront de cette section et rejoindront les pages d'architecture au fur et à mesure de leur réalisation.
- Recherche sémantique (pg_vector) — réservé à de futures fonctionnalités de recommandation. Non installé. (PostGIS, l'autre extension visée, est désormais implémentée : colonne générée
providers.location geography(Point), index GiST, tri KNNlocation <-> pointdes endpoints de recherche — voir Base de données. La question du paquet Eloquent spatial est tranchée par la négative : SQL brut uniquement, aucun type spatial n'est hydraté côté PHP.) - Cloisonnement des données (tenancy) — l'isolation par organisation au niveau des lignes (global scopes Eloquent + policies) n'est pas encore en place. Le middleware
ResolveViewingContext(X-Viewing-As) fixe déjà le scope d'organisation viasetPermissionsTeamId()pour chaque requête (voir Identité & accès) ; la couche globale scopes / policies est la prochaine étape. - Cloisonnement des tokens mobiles — les token abilities de Sanctum pour séparer les tokens mobiles des endpoints des portails (défense en profondeur) ne sont pas encore exploitées.
- Notifications push — FCM / APNs et l'ordonnancement (scheduler + queues) pour débloquer les étapes du road-trip dépendent de la mécanique de révélation ; non implémentés.
- Routage d'étape — le cœur est désormais construit : la table
step_routings(avecrevealed_at— « révélée » est l'horodatage, plus l'existence de la ligne), le cycle de vie retrait / résurrection des alternatives (activity_step.retired_at, priorité nullable, unicité différée, édition par différence), la timeline voyageur à révélation progressive (GET /activation-codes/{id}/steps+POST …/steps/{stepId}/reveal, instant de révélation calculé et imposé côté serveur), l'écran de détail d'une étape révélée avec le contenu réel du plan suivi (GET /activation-codes/{id}/steps/{stepId}, lu par la ligne de routage : un plan retiré ou une activité archivée restent affichables), la survie de ces deux lectures au voyage — un voyage terminé garde sa timeline et ses souvenirs, tandis que la révélation, elle, se ferme avec le voyage et scelle définitivement ce qui n'a pas été vu (onglet « Passés » de l'espace voyageur), la demande d'un autre plan par le voyageur (POST …/steps/{stepId}/next-plan: déroutage instantané et irréversible vers l'alternative vivante suivante, ouvert entre le rendez-vous de l'étape et le démarrage de la suivante — plafonné par la fin du voyage —, le contrat se réduisant à la paire d'instantsnext_plan_requestable_at/_until: aucun rang ne circule) — et le (pré-)routage super-admin par voyageur (PUT /tour/step/{stepId}/routings) — voir Routage d'étape, Alternative d'étape et Base de données. Reste à construire : l'UI super-admin du routage — dont le pré-routage par cohorte (« ces 5 voyageurs sur A, les 7 suivants sur B ») pour lequel le curseur de plan est déjà prêt côté données — et le point de rendez-vous de l'écran de détail (texte libre « rendez-vous à l'embarcadère… » — pas de colonne à ce jour ;schedule_notesest une note de disponibilité héritée, pas un lieu de rendez-vous) ainsi qu'une éventuelle seconde photo (une activité ne porte qu'une image). Le socle capacité & saturation est construit :activities.capacity/unlimited, les fermetures datées (activity_closures), la taille du groupe (activation_codes.traveller_count, écrite atomiquement avecused_at/tour_idà l'activation) et les endpoints de lecture capacité / plénitude (tour et région), comme le choix du tour à l'activation (voir Capacité & saturation, Code d'activation et Base de données). Trois points consciemment reportés :- le cas « tous les plans sont complets » — non traité ; la porte de sortie prévue est d'ajouter une alternative à l'étape puis de router vers elle, jamais d'assouplir la clé composite ;
- les éditions structurelles planifiées (« la mise à jour du tour aura lieu à 22 h ») — idée flottée par le client, non confirmée ; la révélation qui fige le routage rend la plupart des éditions déjà sûres sans planificateur, seul un garde-fou « voyageurs en route aujourd'hui » est prévu.
activation_codes.durationest nullable (les imports hérités n'en portent pas) alors que la brancheactivede la vue de statut l'exige : un tel code n'estactiveaucune seconde du voyage qu'il vit. Les lectures de la timeline s'en accommodent (elles suiventused_at), mais la révélation reste fermée à ces codes. À trancher côté donnée — duration obligatoire à l'activation, ou valeur par défaut — plutôt qu'en élargissant la garde d'écriture.
- Domaine & API — sont implémentés : l'entité
ActivationCode(codes d'accès remis par les revendeurs / agences) avec son action de génération par lot et l'entitéActivationCodeBatch(lots de première classe : notes internes, expiration pilotée au niveau du lot, endpoints/api/activation-codes/batches) ; la couche de données et les endpoints prestataires / activités (Provider,Activity, le registre de tagsTag/ActivityTaget son CRUD/api/tags) ; et la planification des road-trips (Tour,Stepet leurs endpoints/api/tour/...— voir Base de données et la matrice d'accès d'Identité & accès). Restent à construire pour les prestataires : l'import/api/import/providerset l'entité « destinations » (l'ancienfk_destinationest reconstructible viaproviders.legacy_pk). - Gestion des images — liaison aux entités — l'infrastructure (disque
s3, MinIO en dev, imgproxy, signeur d'URLs), la tableimageset l'uploadPOST /api/imagessont construits (voir Stockage d'images), tout comme les variantes de tailles (ImageVariant) et les liaisons photo de profil (users.profile_picture_id), image d'activité (activities.image_id) et image principale d'organisation (organizations.primary_image_id) ; restent la liaison images ↔ prestataires et les presets imgproxy. app/Actions/— répertoire introduit avecGenerateActivationCodesAction. Contient les opérations de domaine trop complexes pour un contrôleur mais ne nécessitant pas de service injecté ; convention : une classe, une méthode publiqueexecute().app/Services/— répertoire introduit avecImgproxyUrlSigner. Contient les services injectables pilotés par la configuration (liés dansAppServiceProvider), par opposition aux opérations de domaine deapp/Actions/.
Contributors
No contributors
Changelog
No recent changes

