PROMPT BACKEND — MyKollector® v5
==================================
Architecture : Backend PHP/Slim sur OVH mutualisé Pro
Base de données : MySQL OVH Pro (accès via PDO)
Coût supplémentaire : 0€

Le document en annexe (07-MyKollectOr-FAB-COMPLETV05.docx) est la documentation
technique complète de la plateforme MyKollector®.
Utilise-le comme référence unique et exhaustive. Ne complète pas avec des suppositions.

---

MISSION
-------
Génère le backend complet de MyKollector® en PHP avec Slim Framework 4.
Le backend est une API REST pure, sans génération de fichiers DXF ni PDF.
Ces tâches sont déléguées à l'agent local installé sur le PC du fabricant.

STACK IMPOSÉE
-------------
- PHP 8.1+ (disponible sur OVH Pro mutualisé)
- Slim Framework 4 (micro-framework REST, équivalent Flask)
- PHP-DI (injection de dépendances pour Slim 4)
- Eloquent ORM (Laravel) standalone — gestion des modèles et migrations
- firebase/php-jwt (JWT 24h)
- PHPMailer (envoi SMTP synchrone)
- vlucas/phpdotenv (variables d'environnement)
- ramsey/uuid (génération UUIDs)
- guzzlehttp/guzzle (appels HTTP vers PrestaShop, Strava et webhooks externes)
- tuupola/slim-jwt-auth (middleware JWT pour Slim)
- Pas de Redis, pas de workers asynchrones, pas de queues

CE QUE LE BACKEND NE FAIT PAS (délégué à l'agent local)
---------------------------------------------------------
- Génération de fichiers DXF
- Génération de PDFs étiquettes
- Jobs asynchrones en arrière-plan

CE QUE LE BACKEND FAIT
-----------------------
- Authentification JWT pour tous les rôles
- CRUD complet de toutes les entités
- Fournir les données à l'agent local via endpoints dédiés
- Enregistrer les rapports de génération envoyés par l'agent
- Gérer les statuts de commandes et remonter vers PrestaShop
- Chat interne (polling toutes les 5s)
- Notifications email SMTP (envoi synchrone via PHPMailer)
- Sync PrestaShop (appelée manuellement ou via cron OVH)
- Sync B2B PrestaShop : commandes B2B (mairies/boutiques) arrivent directement chez le fabricant
- Intégration Strava (OAuth, détection GPS Haversine)
- Détection de zone d'approche GPS (distance_approche_m configurable par parcours)
- Export parcours vers destinations multiples (webhooks, événements internes, plateformes tierces)
- Console super admin
- Logs et audit
- Health check GET /api/health

INFRASTRUCTURE CIBLE
---------------------
- Hébergement : OVH mutualisé Pro (Apache + PHP 8.1+)
- Base de données : MySQL fourni par OVH Pro (accès via PDO/Eloquent)
- Déploiement : dépôt FTP/SFTP ou Git (selon accès SSH OVH)
- HTTPS : certificat SSL Let's Encrypt OVH (activé depuis l'espace client)
- Sous-domaines gérés depuis l'espace client OVH
- Pas de Docker, pas de systemd, pas de VPS

STRUCTURE DE FICHIERS ATTENDUE
-------------------------------
  backend/
  ├── public/
  │   └── index.php           ← point d'entrée unique (front controller)
  ├── src/
  │   ├── App/
  │   │   ├── Controllers/
  │   │   │   ├── AuthController.php
  │   │   │   ├── UserController.php
  │   │   │   ├── ParcoursController.php
  │   │   │   ├── CommandeController.php
  │   │   │   ├── ChronoController.php
  │   │   │   ├── GravureController.php
  │   │   │   ├── EtiquetteController.php
  │   │   │   ├── AgentController.php
  │   │   │   ├── ChatController.php
  │   │   │   ├── NotificationController.php
  │   │   │   ├── StravaController.php
  │   │   │   ├── PrestashopController.php
  │   │   │   ├── ParcoursExportController.php   ← NOUVEAU : export multi-API
  │   │   │   ├── SavController.php
  │   │   │   ├── RapportController.php
  │   │   │   ├── AdminController.php
  │   │   │   └── HealthController.php
  │   │   ├── Models/
  │   │   │   ├── User.php
  │   │   │   ├── Produit.php
  │   │   │   ├── Parcours.php
  │   │   │   ├── Chrono.php
  │   │   │   ├── Commande.php
  │   │   │   ├── Lot.php
  │   │   │   ├── CommandeLot.php
  │   │   │   ├── FichierGravure.php
  │   │   │   ├── CommandeFichierGravure.php
  │   │   │   ├── TemplateGravure.php
  │   │   │   ├── TemplateEtiquette.php
  │   │   │   ├── Atelier.php
  │   │   │   ├── Fabricant.php
  │   │   │   ├── ChatConversation.php
  │   │   │   ├── ChatParticipant.php
  │   │   │   ├── ChatMessage.php
  │   │   │   ├── Notification.php
  │   │   │   ├── LogEntry.php
  │   │   │   ├── SavTicket.php
  │   │   │   ├── Defaut.php
  │   │   │   ├── Rapport.php
  │   │   │   ├── RapportGeneration.php
  │   │   │   ├── RelanceSeuil.php
  │   │   │   ├── PrestashopConfig.php
  │   │   │   ├── StravaConfig.php
  │   │   │   ├── Stock.php
  │   │   │   ├── Classement.php
  │   │   │   ├── ExportDestination.php          ← NOUVEAU
  │   │   │   └── Session.php
  │   │   ├── Middleware/
  │   │   │   ├── JwtMiddleware.php
  │   │   │   ├── RoleMiddleware.php
  │   │   │   ├── RateLimitMiddleware.php
  │   │   │   ├── CorsMiddleware.php
  │   │   │   └── MaintenanceMiddleware.php
  │   │   └── Services/
  │   │       ├── JwtService.php
  │   │       ├── MailService.php
  │   │       ├── StravaService.php
  │   │       ├── PrestashopService.php
  │   │       ├── ParcoursExportService.php      ← NOUVEAU
  │   │       └── EncryptionService.php
  │   └── bootstrap.php       ← initialisation app, DI container, DB
  ├── database/
  │   └── migrations/         ← fichiers de migration Eloquent/Phinx
  ├── storage/
  │   └── templates_dxf/      ← templates DXF uploadés via admin (servis à l'agent)
  ├── logs/
  │   └── app.log
  ├── .htaccess               ← redirection vers public/index.php + HTTPS forcé
  ├── .env.example
  ├── composer.json
  └── README_DEPLOIEMENT.md

CONFIGURATION .htaccess (CRITIQUE pour OVH mutualisé)
------------------------------------------------------
Le fichier .htaccess à la racine doit rediriger toutes les requêtes vers public/index.php :

  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^ public/index.php [QSA,L]

  # Forcer HTTPS
  RewriteCond %{HTTPS} off
  RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

MODÈLES ET BASE DE DONNÉES
---------------------------
28 tables MySQL via Eloquent ORM :
users, produits, parcours, chronos, commandes, lots, commande_lot (pivot),
fichiers_gravure, commande_fichier_gravure (pivot), templates_gravure,
templates_etiquettes, ateliers, fabricants, chat_conversations,
chat_participants, chat_messages, notifications, logs_audit, sav_tickets,
defauts, rapports, rapports_generation, relance_seuils, prestashop_config,
strava_config, stocks, classements, sessions, export_destinations

Table produits (parcours) — champs COMPLETS :
- id, slug (unique), nom, categorie (cyclisme/trail/rando)
- altitude, distance_km, denivele, difficulte, sous_titre, description
- actif (bool), message_desactivation
- sommet_lat, sommet_lng                   ← coordonnées GPS sommet
- depart_lat, depart_lng                   ← coordonnées GPS départ (= point de départ segment)
- rayon_km DECIMAL(4,2) DEFAULT 1.0        ← rayon de détection Strava (en km)
- distance_approche_m INT DEFAULT 500      ← NOUVEAU : distance en mètres à partir de laquelle
                                              le chrono QR passe en mode "approche active"
                                              (configurable par le super admin, défaut 500m)
- strava_keywords VARCHAR(200)
- shop_synced (bool), presta_product_id, lien_boutique, qr_url
- created_at, updated_at

RÈGLE MÉTIER — distance_approche_m :
Le champ distance_approche_m est servi par GET /api/parcours/:id.
Sur la page QR, le JS calcule en temps réel la distance Haversine entre la
position GPS du sportif et le point depart_lat/depart_lng.
- Si distance > distance_approche_m : affichage "Approchez-vous du départ (Xm restants)"
- Si distance <= distance_approche_m : le bouton PARTIR devient actif (mode "approche atteinte")
- Si pas de GPS disponible (hors ligne, refus) : le bouton PARTIR est actif immédiatement
Le chrono ne démarre qu'au clic sur PARTIR — la géolocalisation conditionne l'activation
du bouton, pas le démarrage automatique du chrono.
Le point de départ et le point de début de segment Strava sont identiques (depart_lat/depart_lng).

Table lots (IMPORTANTE) :
- id, statut (en_attente_generation | généré | en_gravure | gravé | en_expedition | expédié)
- template_gravure_id, atelier_id, created_by, created_at, updated_at
- Un lot regroupe plusieurs commandes via la table pivot commande_lot

Table rapports_generation :
- id, lot_id, fichiers (JSON), statut, erreur, created_at, agent_user_id

Table commandes — champs supplémentaires B2B :
- is_b2b BOOLEAN DEFAULT FALSE             ← NOUVEAU : commande B2B (mairie/boutique)
- b2b_client_nom VARCHAR(200) NULL         ← nom de l'organisation B2B
- b2b_client_type ENUM('mairie','boutique','association','autre') NULL
- b2b_fabricant_id INT NULL                ← fabricant assigné directement
- b2b_quantite INT DEFAULT 1               ← quantité commandée (B2B peut commander plusieurs)
- validation_sportif BOOLEAN DEFAULT FALSE ← reste FALSE pour B2B (pas de validation sportif)

Table export_destinations (NOUVELLE) :
- id INT AUTO_INCREMENT PRIMARY KEY
- nom VARCHAR(100) NOT NULL                ← ex: "Genspark Events", "Site Trail du Vercors"
- type ENUM('webhook','prestashop','interne','autre') NOT NULL
- url VARCHAR(500) NOT NULL                ← URL du webhook ou endpoint
- headers JSON NULL                        ← headers HTTP additionnels (Authorization, etc.)
- actif BOOLEAN DEFAULT TRUE
- description TEXT NULL
- created_at, updated_at

STATUTS DE COMMANDE ET FLUX PRESTASHOP
----------------------------------------
Statuts possibles d'un lot/commande :
  en_attente_generation → généré → en_gravure → gravé → en_expedition → expédié

Cas particulier B2B :
  commande_b2b (is_b2b=true) → attente_gravure DIRECTE (pas de chrono, pas de validation sportif)
  → en_gravure → gravé → en_expedition → expédié

Remontées vers PrestaShop :
1. Passage à "en_gravure" (prise en charge du lot par l'opérateur) :
   - Déclenché par PATCH /api/lots/:id/statut { "statut": "en_gravure" }
   - Pour TOUTES les commandes du lot simultanément
   - Appel PrestaShop : PUT /api/orders/:id avec statut custom "En cours de gravage"
   - Si une commande n'a pas d'id_prestashop → loguer et continuer (ne pas bloquer)

2. Passage à "expédié" :
   - Déclenché par PATCH /api/commandes/:id/statut { "statut": "expédié", "numero_suivi": "..." }
   - Appel PrestaShop : PUT /api/orders/:id avec statut natif "Expédié" + numéro de suivi

ENDPOINTS API COMPLETS
-----------------------

Auth :
POST   /api/setup                    ← premier démarrage uniquement
POST   /api/auth/login
POST   /api/auth/logout
POST   /api/auth/refresh

Utilisateurs :
GET    /api/users
POST   /api/users
GET    /api/users/:id
PATCH  /api/users/:id
DELETE /api/users/:id
PATCH  /api/user/profil              ← sportif modifie son nom/prénom (source vérité gravure)

Parcours :
GET    /api/parcours
POST   /api/parcours
GET    /api/parcours/:id             ← retourne aussi distance_approche_m
PATCH  /api/parcours/:id
DELETE /api/parcours/:id
POST   /api/parcours/:id/sync-prestashop
GET    /api/parcours/:id/qr
POST   /api/parcours/:id/export      ← NOUVEAU : envoie le parcours vers une/plusieurs destinations

Chronos :
GET    /api/chronos
POST   /api/chronos
GET    /api/chronos/:id
PATCH  /api/chronos/:id
DELETE /api/chronos/:id

Commandes :
GET    /api/commandes
GET    /api/commandes/:id
PATCH  /api/commandes/:id/statut
GET    /api/commandes?b2b=true&fabricant_id=X  ← NOUVEAU : filtre commandes B2B par fabricant

Lots (gravure) :
GET    /api/lots
POST   /api/lots                     ← crée un lot, regroupe commandes sélectionnées
GET    /api/lots/:id
PATCH  /api/lots/:id/statut          ← passage en_gravure → remonte PrestaShop pour tout le lot
DELETE /api/lots/:id
GET    /api/lots/:id/csv             ← export CSV du lot (colonnes: numero_commande, nom, prenom, chrono, date, parcours)
     Content-Type: text/csv, encodage UTF-8 avec BOM (compatibilité Excel)
     Filename: lot_[id]_[date].csv

Templates gravure :
GET    /api/templates-gravure
POST   /api/templates-gravure        ← upload DXF
GET    /api/templates-gravure/:id
PATCH  /api/templates-gravure/:id
DELETE /api/templates-gravure/:id

Templates étiquettes :
GET    /api/templates-etiquettes
POST   /api/templates-etiquettes
GET    /api/templates-etiquettes/:id
PATCH  /api/templates-etiquettes/:id
DELETE /api/templates-etiquettes/:id

Endpoints agent local :
GET    /api/agent/lots-en-attente              ← lots statut = en_attente_generation
GET    /api/agent/template-gravure/:id/fichier ← téléchargement binaire DXF
POST   /api/agent/lot/:id/rapport              ← agent signale fin génération DXF
GET    /api/agent/etiquettes-en-attente        ← commandes statut = gravé
POST   /api/agent/commande/:id/etiquette-generee ← agent signale PDF généré

Chat (polling) :
GET    /api/chat/conversations
POST   /api/chat/conversations
GET    /api/chat/:conversation_id/messages?since=timestamp
POST   /api/chat/:conversation_id/messages
POST   /api/chat/:conversation_id/pieces-jointes

Notifications :
GET    /api/notifications
PATCH  /api/notifications/:id/lu

Strava :
GET    /api/strava/auth-url
GET    /api/strava/callback
GET    /api/strava/activites
POST   /api/strava/import/:activite_id
DELETE /api/strava/disconnect
POST   /api/strava/webhook

PrestaShop :
POST   /api/prestashop/sync-commandes           ← import commandes B2C (sportifs)
POST   /api/prestashop/sync-commandes-b2b       ← NOUVEAU : import commandes B2B (mairies/boutiques)
POST   /api/prestashop/test-connexion

Export destinations (NOUVEAU) :
GET    /api/admin/export-destinations
POST   /api/admin/export-destinations
GET    /api/admin/export-destinations/:id
PATCH  /api/admin/export-destinations/:id
DELETE /api/admin/export-destinations/:id

SAV :
GET    /api/sav
POST   /api/sav
GET    /api/sav/:id
PATCH  /api/sav/:id

Défauts :
GET    /api/defauts
POST   /api/defauts
GET    /api/defauts/:id

Rapports :
GET    /api/rapports
POST   /api/rapports/export

Admin :
GET    /api/admin/stats
GET    /api/admin/logs
GET    /api/admin/config
PATCH  /api/admin/config
POST   /api/admin/backup
GET    /api/admin/backups
POST   /api/admin/maintenance

Health :
GET    /api/health

MODULE PRESTASHOP (PrestashopService.php)
------------------------------------------
AUTHENTIFICATION UNIQUE — mécanisme exact :
a. Le sportif saisit son email sur chrono.mykollector.com
b. MyKollector interroge l'API WebService REST PrestaShop :
   GET /api/customers?filter[email]=[email]&display=full
   (authentification HTTP Basic avec la clé API PrestaShop en username, password vide)
c. Si le compte existe → création ou liaison du compte MyKollector
d. Si le compte n'existe pas → message d'erreur (pas de création côté PrestaShop)
NE PAS implémenter OAuth.

Import commandes B2C : endpoint appelé manuellement ou via cron OVH.
Import commandes B2B (NOUVEAU) :
- PrestaShop identifie les commandes B2B par un champ custom "b2b_client_type" (mairie/boutique/etc.)
  OU par le groupe client PrestaShop ("Professionnels").
- Lors du sync, si la commande est B2B : is_b2b = TRUE, b2b_fabricant_id est déduit du produit
  (chaque produit parcours a un fabricant_id assigné dans MyKollector).
- Ces commandes apparaissent directement dans l'espace fab du fabricant concerné sans validation sportif.
- La colonne "B2B" est affichée dans le tableau commandes du fab avec badge distinctif.

Push statut "En cours de gravage" : appel PUT /api/orders/:prestashop_id
Push statut "Expédié" : appel PUT /api/orders/:prestashop_id + numéro de suivi

MODULE EXPORT PARCOURS (ParcoursExportService.php) — NOUVEAU
------------------------------------------------------------
Permet d'envoyer les données d'un parcours vers des destinations configurables.
Cas d'usage : créer un événement sur une plateforme tierce, synchroniser
avec un site partenaire, déclencher un webhook interne.

Comportement de POST /api/parcours/:id/export :
- Corps de la requête : { "destination_ids": [1, 2, 3] } (liste d'IDs de destinations actives)
  OU { "destination_ids": "all" } pour envoyer vers toutes les destinations actives
- Pour chaque destination :
  * Type "webhook" ou "autre" : HTTP POST vers destination.url avec payload JSON du parcours
    Headers envoyés : Content-Type: application/json + headers personnalisés de la destination
  * Type "prestashop" : appelle la logique sync-prestashop existante
  * Type "interne" : enregistre dans logs_audit avec action "parcours_exported_interne"
- Payload JSON envoyé :
  {
    "parcours_id": 1,
    "slug": "col-izoard",
    "nom": "Col de l'Izoard",
    "categorie": "cyclisme",
    "altitude": 2360,
    "distance_km": 14.5,
    "denivele": 1100,
    "difficulte": "HC",
    "depart_lat": 44.680,
    "depart_lng": 6.734,
    "sommet_lat": 44.820,
    "sommet_lng": 6.735,
    "rayon_km": 1.0,
    "distance_approche_m": 500,
    "lien_boutique": "https://shop.mykollector.com/...",
    "qr_url": "https://chrono.mykollector.com/qr?p=col-izoard",
    "exported_at": "2026-04-11T10:00:00Z"
  }
- Réponse : { "resultats": [ { "destination_id": 1, "nom": "...", "statut": "ok"|"erreur", "message": "..." } ] }
- Les erreurs d'envoi sont loguées mais ne bloquent pas les autres destinations
- Timeout par requête webhook : 10 secondes

MODULE STRAVA (StravaService.php)
----------------------------------
- OAuth scopes : read, activity:read
- Tokens chiffrés AES-256 en base (via EncryptionService.php + openssl)
- Algorithme de détection GPS Haversine (score 0.6–1.0) — utilise sommet_lat/sommet_lng
- Détection de début de segment : utilise depart_lat/depart_lng (même point que zone d'approche QR)
- Webhook temps réel (endpoint POST /api/strava/webhook)

MODULE CHRONO / ZONE D'APPROCHE GPS
-------------------------------------
La page QR récupère le parcours via GET /api/parcours/:slug et lit :
  - depart_lat, depart_lng  : coordonnées du point de départ
  - distance_approche_m     : distance d'activation du bouton PARTIR (en mètres)

Logique JS côté client (aucun endpoint backend supplémentaire requis) :
  1. navigator.geolocation.watchPosition() → position en temps réel
  2. Calcul Haversine entre position sportif et (depart_lat, depart_lng)
  3. Si distance <= distance_approche_m → bouton PARTIR s'active
  4. Si GPS indisponible/refusé → bouton PARTIR actif immédiatement
  5. Le chrono démarre au clic sur PARTIR (pas de démarrage automatique GPS)
  6. L'affichage indique "À Xm du départ" quand le sportif est en approche

CHAT (polling)
--------------
- GET /api/chat/:id/messages?since=timestamp → retourne les messages depuis timestamp
- Le frontend appelle cet endpoint toutes les 5 secondes
- Pas de WebSocket (non supporté sur mutualisé OVH)
- Règles de visibilité (section 15 du document) :
  * Super admin : voit tout
  * Admin_fab : voit/écrit aux opérateurs de son atelier + super admin
  * Opérateur : voit/écrit à son admin_fab + super admin + opérateurs du même atelier
- Pièces jointes : images (jpg, png, gif), PDF, max 5 Mo

NOTIFICATIONS EMAIL (MailService.php)
--------------------------------------
- Envoi synchrone via PHPMailer
- SMTP configurable (host, port, user, password, from)
- Seuils de relance configurables (table relance_seuils)
- Déclenchement sur changements de statut commande

SÉCURITÉ
---------
- Rate limiting par route (compteur en base MySQL — pas de Redis)
- Circuit breaker : 5 échecs → pause 60s (état stocké en base)
- JWT 24h, refresh token
- CORS : autoriser chrono.mykollector.com et fab.mykollector.com uniquement
- Format d'erreur uniforme : { "error": true, "code": "...", "message": "..." }
- Démarrage sans seed : GET /api/setup bloqué après création du super_admin
- nom/prenom dans users = source de vérité pour la gravure

RÈGLES ABSOLUES
---------------
- Compatibilité MySQL 8+ uniquement (pas de SQLite — OVH Pro = MySQL)
- Pas de Docker, pas de systemd, pas de Redis
- Toutes les réponses en JSON
- Les uploads (DXF, pièces jointes) stockés dans storage/ (hors public/)
- Le dossier public/ est le seul accessible depuis le web

LIVRABLES
---------
- Tous les fichiers PHP fonctionnels
- composer.json avec toutes les dépendances et versions fixes
- .env.example avec toutes les variables commentées
- .htaccess (racine + public/)
- database/migrations/ (fichiers de migration)
- Script d'installation : install.php (crée les tables, vérifie l'environnement)
- README_DEPLOIEMENT.md : dépôt FTP sur OVH mutualisé, étape par étape
