╔══════════════════════════════════════════════════════════════════════════════╗
║  MYKOLLECTOR® — PROMPT PHASE 2 : BACKEND API                               ║
║  À utiliser avec : MKO_API.docx + MKO_BDD.docx                             ║
╚══════════════════════════════════════════════════════════════════════════════╝

## CONTEXTE

Tu vas générer le backend complet de api.mykollector.com — la pièce centrale
de toute la plateforme MyKollectOr®. Chaque domaine (chrono, fab, workflow)
dépend de cette API. La bible technique est fournie dans MKO_API.docx et
MKO_BDD.docx — elles font autorité.

## CONTRAINTES TECHNIQUES ABSOLUES

- Hébergement : OVH Mutualisé Pro — PHP 8.1 — pas de Docker
- Framework : Slim Framework 4 + Eloquent ORM standalone
- Auth : firebase/php-jwt + tuupola/slim-jwt-auth
- IoC : PHP-DI
- Email : PHPMailer — SMTP ssl0.ovh.net:587:tls
- HTTP Client : guzzlehttp/guzzle (appels PS OAuth2 + Strava + Dropbox)
- Utilitaires : ramsey/uuid + vlucas/phpdotenv
- Chiffrement : openssl_encrypt AES-256-CBC (tokens Strava + token Dropbox)
- Point d'entrée unique : public/index.php
- CORS : uniquement https://chrono.mykollector.com et https://fab.mykollector.com
  et https://workflow.mykollector.com

## STRUCTURE DE FICHIERS À GÉNÉRER

api.mykollector.com/
├── public/
│   └── index.php                    ← Point d'entrée Slim 4
├── src/App/
│   ├── Controllers/
│   │   ├── AuthController.php       ← register, login, logout, refresh, me, profil, forgot, reset
│   │   ├── ParcoursController.php   ← CRUD parcours + tracking_config
│   │   ├── ChronoController.php     ← start, stop, valider, manuel, history
│   │   ├── CommandeController.php   ← mes commandes, valider-gravure
│   │   ├── StravaController.php     ← auth-url, callback, activites, import, webhook, alertes, disconnect
│   │   ├── FabController.php        ← commandes fab, lots, gravure, expedition, SAV, defauts
│   │   ├── AgentController.php      ← lots-en-attente, template, rapport, etiquettes, heartbeat
│   │   ├── PrestashopController.php ← sync-commandes, sync-b2b, test-connexion
│   │   ├── ChatController.php       ← conversations, messages, envoyer
│   │   ├── NotificationController.php
│   │   ├── AdminController.php      ← stats, logs, config, users, backup, setup, agent-status
│   │   ├── ContactController.php    ← CRUD contacts + creer-acces-fab
│   │   ├── ClassementController.php
│   │   └── UserController.php       ← export RGPD, suppression compte
│   ├── Models/
│   │   ├── User.php
│   │   ├── Produit.php
│   │   ├── SegmentTrackingConfig.php
│   │   ├── Chrono.php
│   │   ├── Commande.php
│   │   ├── Lot.php
│   │   ├── CommandeLot.php
│   │   ├── FichierGravure.php
│   │   ├── TemplateGravure.php
│   │   ├── Atelier.php
│   │   ├── Stock.php
│   │   ├── Classement.php
│   │   ├── SavTicket.php
│   │   ├── Notification.php
│   │   ├── LogEntry.php
│   │   ├── Session.php
│   │   ├── RelanceLog.php
│   │   └── ChatMessage.php
│   ├── Middleware/
│   │   ├── JwtMiddleware.php        ← Vérifie JWT — extrait user_id + role
│   │   ├── RoleMiddleware.php       ← Vérifie le rôle requis
│   │   ├── RateLimitMiddleware.php  ← Rate limiting par IP (fichier JSON)
│   │   ├── CorsMiddleware.php       ← CORS strict 3 domaines
│   │   └── MaintenanceMiddleware.php
│   └── Services/
│       ├── JwtService.php           ← generate, verify, refresh
│       ├── MailService.php          ← PHPMailer — tous les emails transactionnels
│       ├── StravaService.php        ← OAuth2, webhook, déduplication v1.1
│       ├── PrestashopService.php    ← OAuth2 PS, import commandes, push statuts
│       ├── GravureService.php       ← Génération texte gravure par type commande
│       ├── BackupService.php        ← mysqldump + upload Dropbox
│       ├── GroqService.php          ← Appels API Groq llama-3.3-70b-versatile
│       └── RelanceService.php       ← Algorithmes relances J+7/J+30/J+60
├── cron/
│   ├── webhook-retry.php            ← Retry imports PS en échec (queue JSON)
│   ├── relances.php                 ← Relances chrono non converti J+7/J+30/J+60
│   ├── relance-gravure-j3.php       ← Relance confirmation gravure J+3
│   ├── confirmation-auto.php        ← Auto-validation gravure J+7
│   ├── stats-cache.php              ← Refresh cache statistiques
│   ├── backup.php                   ← Backup DB + upload Dropbox hebdo
│   ├── strava-refresh.php           ← Refresh tokens Strava expirants
│   ├── stock-alerts.php             ← Alertes stock bas
│   └── agent-heartbeat-check.php   ← Vérif silence agents > 15 min
├── storage/
│   ├── templates_dxf/               ← Templates DXF par parcours
│   ├── etiquettes_pdf/              ← PDF étiquettes générés par dompdf (PAS WeasyPrint)
│   ├── queue/                       ← SUPPRIMÉ — file retry PS gérée en table MySQL ps_import_queue
│   └── backups/                     ← Sauvegardes DB locales (rétention 3 niveaux)
├── logs/                            ← app.log rotation quotidienne
├── .env                             ← Variables environnement
├── .htaccess                        ← RewriteRule + HTTPS forcé + protection .env
└── composer.json

## RÈGLES MÉTIER CRITIQUES SUPPLÉMENTAIRES

### users.nom et users.prenom — NULL autorisé
Ces champs sont NULL (pas '') en MySQL STRICT_TRANS_TABLES (OVH MySQL 8.0).
Le code doit traiter NULL correctement :
- À l'affichage : null ?? '' ou null ?? 'Utilisateur'
- Pour le texte gravure : si nom ou prenom NULL → 422 PROFIL_INCOMPLET avant de générer le texte
- À l'inscription : envoyer toujours nom et prenom dans le INSERT (jamais les omettre)

### PATCH /api/user/profil — Blocage si commande en cours
Avant toute modification de prenom ou nom :
SELECT COUNT(*) FROM commandes WHERE user_id=? AND statut IN ('en_lot','en_gravure')
Si COUNT > 0 → rejeter 422 { code: NOM_VERROUILLE_COMMANDE_EN_COURS }
Message : "Impossible de modifier votre nom — une médaille est en cours de gravure avec ce nom."

### DELETE /api/user/data — Double confirmation RGPD
Requiert dans le body :
{ "confirmation": "SUPPRIMER MON COMPTE", "case_cochee": true }
Rejeter 400 si case_cochee !== true ou confirmation !== "SUPPRIMER MON COMPTE"
Rejeter 409 si commande en statut en_lot ou en_gravure
JAMAIS supprimer les commandes — uniquement anonymiser users (nom, prenom, email → NULL + anonymized_at)

### File retry PS — Table MySQL (pas fichier JSON)
Dans cron/webhook-retry.php et PrestashopService.php :
Lock léger sur ps_import_queue — PAS de SELECT FOR UPDATE (deadlock OVH Mutualisé) :
UPDATE ps_import_queue SET locked_at=NOW(), statut='en_cours'
WHERE id=? AND (locked_at IS NULL OR locked_at < NOW()-INTERVAL 30 SECOND)
Si 0 rows affected → skip (déjà lockée par un autre processus)
Après 5 échecs → statut=echec_permanent + email alerte admin + notification M01
Ne plus jamais écrire dans storage/queue/ — cette table remplace le fichier JSON

### Monitoring appels Groq
Dans GroqService.php : logger chaque appel dans logs_audit (action=groq_call)
Lire la limite dans admin config (clé groq_daily_limit, défaut 100)
Si limite atteinte → throw GroqLimitException → les modules affichent "IA indisponible" sans bloquer
Réinitialiser le compteur quotidien à minuit (vérifier dans la méthode call())

## RÈGLES DE SÉCURITÉ CRITIQUES — NE PAS DÉROGER

### Auth JWT
- JWT secret minimum 64 chars depuis .env JWT_SECRET
- Durée access token : 86400s (24h)
- Durée refresh token : 2592000s (30j) — stocké dans table sessions
- Refresh token rotation à chaque renouvellement (révoque l'ancien)

### date_debut chrono — CÔTÉ SERVEUR UNIQUEMENT
- POST /api/chrono/start : date_debut = NOW() côté serveur
- Si le client envoie une date_debut, tolérer ±60s maximum
- Logger SYSTÉMATIQUEMENT l'écart réel dans logs_audit (action=chrono_start_offset, details={ecart_secondes, user_id, produit_id})
- Écart > 60s : rejeter 422 ECART_DATE_TROP_GRAND

### Webhook Strava — vérification obligatoire
- POST /api/strava/webhook : vérifier subscription_id === strava_config.webhook_id
- Vérifier owner_id existe dans users.strava_id
- Si invalide : retourner 200 silencieux (pour ne pas désabonner le webhook) + logger

### Auth agent DXF — API key scopée
- L'agent utilise une API key dédiée (pas email/password)
- La clé est scopée uniquement aux endpoints /api/agent/*
- Générer la clé depuis fab.mykollector.com/admin → un champ api_key dans users
- Logger agent_id à chaque appel

### Scope atelier fabricant
- Tous les endpoints /api/fab/* : vérifier commande.atelier_id === user.atelier_id
- Un opérateur ne peut JAMAIS voir ou modifier les commandes d'un autre atelier

### texte_gravure — VERROUILLÉ
- Le champ texte_gravure dans commandes ne peut être modifié qu'à l'INSERT
- Aucun endpoint ne permet de l'écraser après création — même admin
- Logger toute tentative de modification dans logs_audit

### POST /api/setup
- Requiert header X-Setup-Token correspondant au SETUP_TOKEN généré par install.php
- Bloqué si table users non vide
- Invalider le token après 1er usage

### forgot-password — rate limit par IP uniquement
- Pas de rate limit par email (leak side-channel)
- Rate limit par IP : 3 requêtes / 1h
- Réponse identique si email connu ou inconnu

## GÉNÉRATION DU TEXTE DE GRAVURE (GravureService.php)

Règles exactes selon type_commande :

medaille_complete :
  Format : "PRENOM NOM / NOM_PARCOURS / JJ.MM.AAAA / H'MM\"SS"
  Source : chronos.user_id → users (prenom, nom) + produits.nom + chronos.date_debut + chronos.duree_secondes
  Exemple : "JULIEN MARTIN / COL DE L'IZOARD / 04.05.2026 / 3'12\"42"

gravure_seule :
  Format : identique medaille_complete
  Source : champs personnalisation PrestaShop (prenom, nom, parcours, date, temps saisis par sportif)
  validation_sportif = 1 dès l'import (PAS d'email de confirmation — saisie PS = accord)

b2b_vierge :
  Format : "NOM_ORGANISATION / NOM_PARCOURS"
  Source : commandes.b2b_client_nom + produits.nom
  Exemple : "SPORT ÉVÉNEMENTS SARL / ALPE D'HUEZ"

test_interne :
  Format : "TEST / NOM_PARCOURS / JJ.MM.AAAA"

Règles de formatage :
- Tout en MAJUSCULES
- Séparateur : " / " (espace-slash-espace)
- Date : format JJ.MM.AAAA
- Temps : H'MM"SS (ex: 3'12"42)
- Longueur max : 120 caractères
- Caractères interdits < > " & → remplacer par leurs équivalents

## ALGORITHME STRAVA — DÉDUPLICATION v1.1

Dans StravaService.php, méthode traiterWebhook() :

1. Vérifier subscription_id + owner_id (sécurité)
2. Récupérer l'activité Strava via API (access token chiffré AES-256-CBC)
3. Pour chaque parcours dans produits où actif=1 :
   a. Calculer score de détection (mots-clés strava_keywords + GPS proximity)
   b. Si score >= 0.75 : traiter ce parcours
   c. Si score 0.50-0.74 : créer notification type=strava_score_insuffisant
   d. Si score < 0.50 : ignorer silencieusement
4. Pour chaque parcours avec score >= 0.75 :
   a. Vérifier strava_activity_id dans chronos (déduplication — INDEX idx_strava_activity)
   b. Si déjà importé : already_imported — stop
   c. Chercher chrono QR dans fenêtre strava_dedup_window_minutes (défaut 120 min)
      INDEX idx_user_produit_date (user_id, produit_id, date_debut) — SELECT FOR UPDATE
   d. Si chrono QR trouvé : UPDATE chrono SET strava_activity_id, strava_confirmed=1
   e. Si pas de chrono QR : INSERT nouveau chrono (source='strava', auto_valide=1)

## EMAILS — MailService.php

Implémenter tous les emails listés dans MKO_GLOBAL §8 :
- inscription_sportif : Bienvenue + lien chrono.mykollector.com
- chrono_valide : Temps réalisé + lien boutique
- commande_en_gravure : Lien validation texte 48h (SAUF gravure_seule)
- relance_validation_j3 : Rappel lien (SAUF gravure_seule)
- auto_validation_j7 : Info gravure lancée automatiquement
- expedition : Numéro suivi + transporteur
- rejet_commande : Motif + contact SAV
- relance_chrono_j7/j30/j60 : Séquence relances
- reset_password : Lien unique expire 1h
- acces_fab_cree : Identifiants opérateur fabricant
- stock_bas : Alerte seuil stock
- agent_silence : Alerte agent DXF inactif > 15 min
- backup_dropbox_erreur : Alerte upload Dropbox en échec

## CRONS — Règles algorithmes

### relances.php (quotidien 06h00)
1. SELECT chronos WHERE statut='valide' AND created_at <= NOW() - INTERVAL 7 DAY
2. Pour chaque chrono : vérifier commande existante → ignorer
3. Vérifier relances_log (chrono_id, type_relance='j7_chrono') → ignorer si existe
4. Envoyer email + INSERT relances_log
5. Même logique J+30 et J+60

### confirmation-auto.php (quotidien 07h00)
1. SELECT commandes WHERE statut='importee' AND validation_sportif=0
   AND created_at <= NOW() - INTERVAL 7 DAY
   AND type_commande != 'gravure_seule' (gravure_seule = déjà validation_sportif=1)
2. UPDATE validation_sportif=1, statut='validee'
3. Envoyer email auto_validation_j7
4. Logger logs_audit

### relance-gravure-j3.php (quotidien 07h30)
1. SELECT commandes WHERE statut='importee' AND validation_sportif=0
   AND created_at <= NOW() - INTERVAL 3 DAY
   AND type_commande = 'medaille_complete' (pas gravure_seule)
2. Vérifier relances_log (commande_id, type_relance='j3_gravure') → ignorer
3. Envoyer email relance_validation_j3 + INSERT relances_log

## CAS PARTICULIERS À GÉRER

### Token Strava illisible (ENCRYPTION_KEY changée)
Dans StravaService.php, tout déchiffrement doit être dans try/catch :
- Exception openssl → logger logs_audit action=strava_token_decrypt_error
- Retourner strava_connected=false au client
- NE JAMAIS laisser crasher le backend
- Le sportif voit un message "Reconnectez votre compte Strava"

### Crons OVH — configuration
Les crons sont configurés dans le Manager OVH → Hébergements → Tâches planifiées.
OVH gère automatiquement le chemin PHP — sélectionner PHP 8.1 dans le menu déroulant.
Chaque fichier cron commence par :
<?php
define('BASE_PATH', dirname(__DIR__));
require_once BASE_PATH . '/vendor/autoload.php';

## FORMAT DE LIVRAISON ATTENDU

Livrer l'arborescence complète avec tous les fichiers listés ci-dessus.
Chaque fichier doit être complet et fonctionnel — pas de TODO ni de placeholder.
Le composer.json doit lister toutes les dépendances avec leurs versions.
Le .htaccess doit inclure la protection contre l'accès direct à .env et storage/.

Tester mentalement chaque endpoint contre les specs de MKO_API.docx avant de livrer.
