Files
projet_gestion_commande/.claude/skills/comprehension-metier/SKILL.md
T
Xor290 5cd8ada828
Backend - Build & Lint / build (push) Failing after 31m35s
chore: build
2026-07-06 21:22:30 +02:00

21 KiB
Raw Blame History

name, description
name description
comprehension-metier Charge le modèle métier complet de la plateforme de gestion de commandes/livraison (rôles, cycle de vie des commandes, stock, catalogue, points/récompenses, parrainage, pénalités, paiements, GPS/assignation, alertes, paramètres configurables). À invoquer avant toute analyse, debug ou modification qui touche à la logique métier — pas seulement au code — pour raisonner avec les vraies règles du business plutôt qu'avec des hypothèses.

Compréhension métier — Plateforme de gestion de commandes/livraison

Référence condensée mais complète du domaine, construite à partir du README.md, des modèles Go (models/) et du code des handlers/DB. Objectif : éviter de raisonner uniquement "à partir du code" sans connaître les règles métier réelles, ce qui est la source la plus fréquente de bugs silencieux dans ce projet (stock, remboursements, idempotence, paramètres codés en dur au lieu de suivre AppSettings).

Contexte général

Plateforme de commande + livraison ("Milieu-Nantais", contact Telegram MLN44LA) avec catalogue produit par catégories (ex. pools de points nommés "Cannabis", "Accessoires" dans les settings par défaut), paiement cash ou crypto, livreurs géolocalisés avec assignation automatique, et un livreur dispose d'un bouton d'alerte police en cas de contrôle/danger pendant une livraison. Cette nature du produit (aucune auto-inscription client, alerte police, paiement crypto natif, pénalités dissuasives sur annulation tardive) doit rester présente à l'esprit : les règles de sécurité et de discrétion opérationnelle (VPN, filtrage des données sensibles pour les livreurs, pas de traces inutiles) sont volontaires, pas accidentelles.

Rôles et permissions

Rôle Description Peut faire
client Utilisateur final Panier, checkout, suivi commande, approuver/annuler, parrainage, points/récompenses, profil, 2FA
admin Gestion complète Tout : produits, clients, commandes, livreurs, cabine, pénalités, paramètres globaux, reset stats
livreur Livreur assigné Voir ses livraisons (données client filtrées), changer statut, position GPS, queue, alerte police, notifications
cabine Cuisine/préparation Voir items commande, préparer/emballer, assigner livreur, confirmer réception, pénalités client, alertes

Règles clés (dont certaines issues du changelog sécurité v5.4.0) :

  • Aucune auto-inscription — les comptes clients sont créés uniquement par un admin (POST /api/v2/admin/protected/clients). Un nouvel endpoint d'inscription libre serait une régression de sécurité majeure.
  • Création de comptes admin entièrement bloquée côté application — un compte admin ne peut être créé qu'en base de données directement, jamais via l'API, quel que soit le rôle appelant (y compris un autre admin).
  • cabine n'a plus aucun droit de création d'utilisateurs ou de clients (retiré côté backend en v5.4.0) — seul admin crée des comptes livreur ou cabine.
  • JWT séparés par famille de rôle : secret client (USER_JWT_SECRET, expiration 5h) ≠ secret admin/livreur/cabine (ADMIN_JWT_SECRET, expiration 10h/2h selon contexte).
  • Chaque action livreur doit vérifier que la commande lui est assignée (livreur_assign == usernameStr), pas seulement le rôle.
  • Filtrage des données sensibles : les livreurs ne reçoivent jamais le téléphone du client dans GET /livreur/deliveries — uniquement nom/prénom. Tout nouvel endpoint livreur exposant des données client doit respecter ce filtrage.

Cycle de vie d'une commande

pending → assigned → en_route → arrived → livre → approved
   ↓          ↓          ↓          ↓         
cancelled (depuis presque tous les états — jamais depuis approved, jamais deux fois de suite)
  • pending : créée au checkout, en attente d'assignation livreur (auto-assign GPS au checkout, ou worker CRON toutes les 1 minute, ou assignation manuelle admin/cabine).
  • assigned : livreur choisi, pas encore parti. Le livreur peut aussi être réassigné manuellement (admin/cabine).
  • en_route : livreur en chemin (start puis mise à jour de statut). ETA calculée (TomTom, fallback Haversine) et stockée dans Redis (command:eta:{id}), utilisée pour les notifications Telegram avec ETA.
  • arrived : livreur à destination — déclenché par le livreur (GPS), ou par admin/cabine via bouton "Le livreur est là" (notify-client). Notifie le client (Telegram). Timer 5 minutes démarre côté app livreur (frontend-admin, DashboardScreen.tsx, ABSENT_TIMEOUT_SECS = 300) → si le client ne descend pas, bouton "Client absent" apparaît.
  • livre : livraison confirmée. Deux voies : validation GPS livreur (distance ≤ 100m de la destination, coordonnées obligatoires) via PUT /livreur/deliveries/:id/status, ou override admin/cabine (force-validate/statut direct). En attente d'approbation client pour finaliser.
  • approved : finalisée. Déclenché par le client (POST /commands/:id/approve avec note + commentaire livreur), ou admin/cabine (confirm-reception/statut direct en override). Points de fidélité attribués à ce moment précis, jamais avant (CalculateAndAddPointsForCommandTx, même transaction que le passage en approved). Terminal — plus aucune modification de stock ou de statut après.
  • cancelled : peut survenir depuis quasiment tous les états précédents. Jamais depuis approved, jamais une seconde fois depuis cancelled (idempotence obligatoire).
  • pending_payment : statut intermédiaire spécifique au paiement crypto (voir section Paiements) — pas dans le cycle "normal", bascule vers pending (paiement confirmé) ou cancelled (paiement échoué/expiré).

Trois chemins de code différents pour l'annulation : CancelCommandAtomic (client), UpdateDeliveryStatus/branche cancelled (livreur — inclut le flux "client absent"), UpdateCommandStatusAdmin (admin/cabine). Toute règle métier touchant l'annulation (remboursement stock, pénalité, notification) doit être répercutée dans les trois, plus CancelCryptoCommand pour le cas crypto.

Correction d'adresse : si une adresse ne peut pas être géocodée ou est jugée invalide, un flux de proposition existe (adresse_correction table, invalid_addresscorrect_address) — le client peut répondre à une proposition (POST /commands/:id/address/respond), l'admin peut modifier l'adresse directement (PUT /orders/:id/address).

Produits, catalogue et tarification

  • Un produit (products) a un stock en float (pas un entier — permet des unités fractionnaires/dosages), une unit, une ou plusieurs catégories, un flag coming_soon (produit visible mais pas encore commandable), et des médias (images).
  • Prix par quantité (product_prices) : chaque palier de quantité a son propre prix et un flag active_price. Un prix désactivé (active_price = false) n'est pas supprimé — juste masqué. Les endpoints publics/client ne renvoient que les prix actifs ; admin et cabine voient tous les prix (actifs et inactifs) pour la gestion complète. Le frontend filtre aussi côté client par sécurité (filter(p => p.active_price !== false)).
  • Désactiver un prix dans l'UI admin (retirer un prix existant) doit désactiver, pas supprimer — cohérence avec l'historique des commandes passées qui référencent ce prix.

Panier et stock

  • Le panier (baskets) vérifie le stock disponible à l'ajout (AddToBasket, rejet si insuffisant) mais ne le réserve pas au sens strict (pas de verrou tant que l'article reste dans le panier) — le stock réel n'est décrémenté qu'à la validation de la commande (checkout), dans une transaction unique avec la création de la commande et le vidage du panier.
  • Un modèle StockInfo distingue Quantity (stock brut), Reserved (quantité présente dans des paniers actifs, à titre indicatif) et Available (Quantity - Reserved) — utilisé pour l'affichage admin, pas comme mécanisme de réservation dur.
  • Articles récompense (is_reward = true, obtenus via le système de points, prix affiché = 0€ mais valeur indicative dans RewardItem.Price) : ce sont des produits physiques réellement distribués. Le stock doit être décrémenté pour eux comme pour un article payant, et remboursé de la même façon en cas d'annulation. Ne jamais les exclure du décompte de stock — seule leur tarification (débit en points au lieu d'euros) diffère.
  • Symétrie obligatoire : toute décrémentation de stock doit avoir un chemin de remboursement, et vice-versa, pour tous les articles sans exception (récompense ou non). Une asymétrie désynchronise durablement le stock affiché de la réalité physique — c'est la classe de bug la plus dangereuse et la plus difficile à détecter de ce projet (corruption silencieuse, cumulative, visible seulement des semaines plus tard).
  • Toute commande annulée deux fois (retry réseau, double-tap, ou canaux différents pour la même commande) ne doit rembourser le stock qu'une seule fois → nécessite un statut "already cancelled" idempotent vérifié dans une transaction verrouillée (FOR UPDATE), pas une simple vérification préalable hors transaction.
  • Créer la commande + insérer les items + décrémenter le stock + vider le panier doivent être une seule transaction — sinon une commande "fantôme" (créée mais jamais payée en stock) peut survivre à un échec de décrément, puis être annulée plus tard et rembourser un stock jamais consommé.

Paramètres globaux configurables (AppSettings)

Presque toutes les règles business ci-dessous sont pilotées par un objet de settings unique, modifiable par l'admin (GET/PUT /api/v2/admin/protected/settings) — ne jamais coder en dur une valeur qui existe déjà comme champ de AppSettings :

Domaine Champs Notes
Pénalités PenaltiesEnabled, ShowAmendeScore, PenaltyTiers[] Tiers par défaut : 0→20€, 1→50€, 2→100€, 3→150€ (voir section Pénalités)
Points PointsEnabled, PointsPools[], PointsReward Pools par défaut : "Pool 1"/"Pool 2" avec barèmes différents (voir section Points)
Parrainage ReferralEnabled, ReferralAmount Montant crédité par défaut = 0 (doit être configuré par l'admin)
Paiement crypto CryptoPaymentEnabled, CryptoOnly, NowPaymentsAPIKey, NowPaymentsIPNSecret, NowPaymentsCurrencies[] CryptoOnly = true désactive le cash
Livraison DeliverySchedule (horaires par jour), PostalZones[] (nom, minimum de commande, codes postaux), DeliveryMode Voir sections dédiées
Telegram TelegramBotToken, TelegramBotUsername, TelegramNotificationsEnabled, Telegram2FAEnabled
Vitrine ShopName (def. "Milieu-Nantais"), ContactTelegram (def. "MLN44LA"), couleurs admin/client, dégradé titre Purement cosmétique

Toute nouvelle règle configurable doit suivre ce même modèle (ajout d'un champ AppSettings + valeur par défaut dans DefaultSettings()) plutôt qu'une constante Go.

Système de points et récompenses (multi-pool)

  • Plusieurs "pools" de points peuvent coexister, chacun associé à un sous-ensemble de catégories de produits (PointsPool.Categories) et avec son propre barème (Tiers : palier de montant dépensé → points gagnés, ex. 3050€ → 1 point, 401€+ → 10 points). Un même achat peut alimenter un pool différent selon la catégorie du produit acheté.
  • Les points cumulés par pool sont stockés hors table clients classique (points_extra/points_redeemed, champs calculés gorm:"-") — lus via GetClientPointsAndRewards.
  • Récompense globale par seuil (PointsReward) : un seuil de points (Threshold) débloque une récompense, dont l'éligibilité est filtrée par catégorie/produits (CategoryConfigs) par pool (seules les catégories appartenant au pool comptent). Le nombre de récompenses disponibles = points_du_pool / Threshold - déjà_réclamées.
  • Réclamation (POST claim, ClaimMyReward) : ajoute les RewardItems définis (produit + quantité) au panier avec is_reward = true et reward_pool_key renseigné — c'est le seul mécanisme qui produit des articles récompense. Consomme une unité de récompense disponible pour ce pool (points_redeemed incrémenté).
  • L'admin peut réinitialiser les récompenses réclamées d'un client pour un pool donné (AdminResetClientRedeemed).

Parrainage (parrain/filleul)

  • Un client peut être parrainé par un autre (clients.parrain). Lier un parrain + créditer le crédit de parrainage (referral_balance, montant = AppSettings.ReferralAmount) doit être atomique (une seule transaction) — sinon un crédit peut être appliqué sans lien enregistré ou l'inverse.
  • Le crédit de parrainage se débite au checkout (DebitReferralBalance) et doit respecter le minimum de la zone de livraison après déduction du crédit (le panier effectif payé doit rester ≥ minimum de la zone du code postal, PostalZones).
  • Si le checkout échoue après débit du crédit (paiement crypto refusé, création de commande en échec), le crédit doit être recrédité (CreditClientReferral) — sinon perte sèche pour le client.
  • Le système peut être entièrement désactivé (ReferralEnabled = false) — vérifier ce flag avant d'exposer une action de parrainage.

Pénalités clients (amendes)

  • Amendes client uniquement, jamais de pénalité livreur. Stockées dans clients.amende, avec compteur cancellations_count et last_penalty_reason.
  • Barème progressif configurable (AppSettings.PenaltyTiers, fallback interne si settings illisibles) — défaut : 1ère annulation 20€, 2ème 50€, 3ème 100€, 4ème+ 150€. Le montant appliqué = penaltyForCount(cancellations_count, PenaltyTiers).
  • Le système entier peut être désactivé (PenaltiesEnabled = false) — dans ce cas le middleware BlockClientIfPenalty laisse passer sans vérification.
  • Blocage du checkout : tant que amende > 0, le middleware BlockClientIfPenalty bloque toute tentative de checkout (403), avec un cache de la pénalité en session Redis (PenaltyCache) pour éviter une lecture DB à chaque requête. Message standard invite à contacter le shop via Telegram pour régulariser.
  • Sources d'amende :
    • Client annule sa propre commande (ApplyCancellationPenalty, incrémente cancellations_count).
    • Livreur marque le client absent depuis le statut arrived (bouton "Client absent", issue_type: client_absent) → ApplyCancellationPenalty appliqué automatiquement au client, jamais au livreur.
    • Admin peut appliquer une pénalité manuelle arbitraire (POST /admin/protected/penalty, montant et raison libres) — indépendante du barème progressif.
  • "Annulation tardive" (règle spécifique au flux client CancelCommandAtomic, distincte du flux "client absent" livreur) = livreur déjà assigné ET (statut en_route/arrived OU ETA valide déjà définie en Redis). Sans livreur assigné ou sans ETA valide → annulation sans pénalité.

Mode d'assignation des livreurs

  • DeliveryMode.Mode : "single" (un seul pool de livreurs, toutes catégories confondues — mode par défaut) ou "category_based" (chaque livreur est routé uniquement vers les commandes contenant les catégories qui lui sont assignées, via CategoryRoutes).
  • En mode category_based, l'auto-assignation GPS doit filtrer les livreurs éligibles par catégorie avant de calculer les distances — une commande mixte (catégories de livreurs différents) est un cas limite à traiter explicitement si cette fonctionnalité est étendue.

GPS, auto-assignation et ETA

  • Géocodage : Nominatim (OpenStreetMap), résultat caché 7 jours (geocode:cache:{hash}).
  • Distance à vol d'oiseau : formule Haversine, calculée localement, aucun appel externe.
  • ETA avec trafic réel : TomTom Routing API. Rotation automatique jusqu'à 3 clés (TOMTOM_API_KEY_1/2/3) — en cas de quota dépassé (403/429), bascule automatique sur la clé suivante sans interruption ; si toutes les clés sont épuisées, fallback sur estimation Haversine + vitesse moyenne 30 km/h (flag fallback_used: true dans la réponse).
  • Auto-assignation : au checkout (immédiate si un livreur est disponible) et via un worker CRON toutes les 1 minute pour les commandes restées pending. Sélectionne le livreur disponible le plus proche avec de la capacité ; si tous sont à capacité maximale, le système peut forcer l'assignation.
  • Capacité de queue : jusqu'à 10 commandes par livreur. Un livreur offline ne reçoit aucune commande.
  • Position GPS livreur stockée dans Redis (delivery:location:{username}, TTL 2h) et diffusée en temps réel via Redis Pub/Sub (channel:position_updates) pour la carte client/admin.
  • Liens de navigation générés vers Google Maps / Waze / Apple Maps / OSM / Bing / Here, pour le livreur comme pour l'admin (supervision).

Paiements

  • Cash (par défaut, sauf si CryptoOnly = true) : le livreur encaisse à la livraison, aucun flux électronique.
  • Crypto (NowPayments) : commande passe en pending_payment en attendant confirmation. Le webhook IPN (POST /webhooks/nowpayments) est public mais signé HMAC-SHA512 (x-nowpayments-sig) — vérifier la signature avant tout traitement, jamais faire confiance au contenu brut. Statuts finished/confirmed → activent la commande (repasse en pending, entre dans le cycle normal) ; failed/expired → annulent et remboursent stock + crédit parrainage.
  • Le stock est décrémenté dès la création de la commande crypto (avant confirmation du paiement) — une commande crypto non payée réserve quand même le stock pendant la fenêtre de paiement, et le libère si elle expire/échoue.
  • CryptoPaymentEnabled = false désactive complètement l'option crypto au checkout ; CryptoOnly = true la rend obligatoire.

Alertes police (sécurité opérationnelle livreur)

  • Un livreur peut déclencher une alerte police à tout moment (POST /livreur/alert, message optionnel) — notifie immédiatement tous les admins et cabine (NotifyAllAdminCabineAlert). C'est un bouton de sécurité personnelle, pas lié à une commande précise.
  • Les alertes peuvent être supprimées par le livreur qui les a créées ou par un admin.

Notifications et 2FA

  • Telegram uniquement — les push Expo sont abandonnées (v5.4.0). Clients, livreurs, admins lient leur compte via un token à usage unique (TTL court, ex. 5 min).
  • Types de notifications : assigned, en_route (avec ETA), arrived, livre, ready_pickup (cabine), address_proposal.
  • 2FA (client) : nécessite Telegram lié + activation admin globale (Telegram2FAEnabled) + toggle personnel du client. Code 6 chiffres, session_token TTL 5 min, rate-limité (429 après trop de tentatives).
  • Le système de notifications peut être désactivé globalement (TelegramNotificationsEnabled = false).

Infrastructure (contexte pour évaluer l'impact d'un changement)

  • Serveurs séparés reliés par VPN WireGuard privé (10.0.0.0/24) : vpn-uber (jump host), monitoring-uber (Wazuh/Dozzle/Beszel), backup-mln (MinIO S3 + ClamAV), bdd-redis-prod (PostgreSQL + Redis, jamais exposé publiquement), prod-uber (backend + WAF, seul serveur public sur 80/443).
  • PostgreSQL et Redis accessibles uniquement via IP VPN (10.0.0.5) depuis prod-uberlatence réseau non négligeable, d'où l'importance de grouper les requêtes (batch inserts, requêtes IN, parallélisation des stats déjà faites dans ce projet).
  • WAF nginx + ModSecurity (OWASP CRS) devant l'API en prod ; logs nginx/ModSecurity montés sur l'hôte pour collecte Wazuh.
  • Déploiement : push sur pre-prod → CI build image Docker (xor1234/backend-mln:pre-prod) → déploiement SSH.
  • Workers automatiques : auto-assignation (1 min), nettoyage queues (5 min), mise à jour ETA (30 s), nettoyage stock (5 min).

Erreurs passées à ne pas reproduire (mémoire vive du projet)

  • Vider le panier avant de décrémenter le stock (au lieu d'une seule transaction) → stock jamais décrémenté en pratique.
  • Restaurer le stock sans vérifier le statut précédent dans une transaction verrouillée → double remboursement sur double-annulation (le livreur avait ce bug, l'admin ne l'avait pas — incohérence entre chemins de code équivalents).
  • Créer la commande + insérer les items avant la transaction de décrément de stock → commande fantôme si le décrément échoue (stock insuffisant détecté trop tard), qui peut ensuite être annulée et rembourser un stock jamais consommé.
  • Exclure les articles récompense du décompte de stock sans les exclure aussi du remboursement (ou l'inverse) → asymétrie, stock qui dérive. Règle définitive validée par l'équipe : les récompenses décrémentent et remboursent le stock exactement comme un article payant.
  • Coder en dur une valeur métier (barème de pénalité, montant de parrainage, seuil de points) qui existe déjà comme champ configurable dans AppSettings — toujours lire les settings, ne jamais dupliquer une constante.