Skip to content

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

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 :

  1. Migration de l'interface d'administration — actuellement réalisée sous FileMaker / HubSpot, vers une nouvelle interface web.
  2. 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 OrganizationType sont reseller (revendeur) et region (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 KNN location <-> point des 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 via setPermissionsTeamId() 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 (avec revealed_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'instants next_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_notes est 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 avec used_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.duration est nullable (les imports hérités n'en portent pas) alors que la branche active de la vue de statut l'exige : un tel code n'est active aucune seconde du voyage qu'il vit. Les lectures de la timeline s'en accommodent (elles suivent used_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 tags Tag / ActivityTag et son CRUD /api/tags) ; et la planification des road-trips (Tour, Step et 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/providers et l'entité « destinations » (l'ancien fk_destination est reconstructible via providers.legacy_pk).
  • Gestion des images — liaison aux entités — l'infrastructure (disque s3, MinIO en dev, imgproxy, signeur d'URLs), la table images et l'upload POST /api/images sont 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 avec GenerateActivationCodesAction. 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 publique execute().
  • app/Services/ — répertoire introduit avec ImgproxyUrlSigner. Contient les services injectables pilotés par la configuration (liés dans AppServiceProvider), par opposition aux opérations de domaine de app/Actions/.

Contributors

No contributors

Changelog

No recent changes