3223 lines
84 KiB
Markdown
3223 lines
84 KiB
Markdown
# 📚 Documentation API - Plateforme de Gestion de Commandes
|
||
|
||
**Version:** 5.4.0
|
||
**Date:** 2026-06-11
|
||
**Base URL prod:** `https://mln-uber.club` (HTTPS via WAF nginx + ModSecurity)
|
||
**Base URL dev:** `http://localhost:8080`
|
||
**Technologies:** Go 1.24, Gin, PostgreSQL 16, Redis 7, React 19 + Vite, Expo 54 (React Native), TomTom API
|
||
|
||
|
||
## 📋 Table des Matières
|
||
|
||
1. [Vue d'ensemble](#vue-densemble)
|
||
2. [Infrastructure Serveurs](#-infrastructure-serveurs)
|
||
3. [Déploiement Production](#-déploiement-production)
|
||
4. [Authentication](#authentication)
|
||
5. [API Client (v1)](#api-client-v1)
|
||
6. [API Admin (v2)](#api-admin-v2)
|
||
7. [API Cabine (v1)](#api-cabine-v1)
|
||
8. [API Livreur (v1)](#api-livreur-v1)
|
||
9. [Notifications Push & Telegram](#-notifications-push--telegram)
|
||
10. [Paiements Crypto](#-paiements-crypto)
|
||
11. [Systeme GPS Integre](#-systeme-gps-integre)
|
||
12. [Codes d'Erreur](#codes-derreur)
|
||
|
||
---
|
||
|
||
## 🎯 Vue d'ensemble
|
||
|
||
Cette API REST complète gère une plateforme de livraison avec assignation automatique GPS des livreurs, suivi en temps réel, et système de pénalités avancé.
|
||
|
||
### ✨ Fonctionnalités Principales
|
||
|
||
- **🔐 Authentication JWT** multi-rôles (Client, Admin, Livreur, Cabine) avec révocation de token
|
||
- **🚗 Auto-assignation GPS** des livreurs les plus proches (worker toutes les 1 minute)
|
||
- **📍 Suivi temps réel** avec ETA et géolocalisation
|
||
- **🔄 Système de queues** Redis optimisé pour les livraisons
|
||
- **⚡ Workers automatiques** (nettoyage, assignation, notifications)
|
||
- **🎯 Système de pénalités clients** — amendes progressives sur annulations (livreurs non concernés)
|
||
- **🤖 Notifications Telegram** pour clients, livreurs et admins (push Expo abandonné)
|
||
- **💸 Paiements crypto** via NowPayments (webhook HMAC)
|
||
- **🛡️ WAF nginx + ModSecurity** (OWASP CRS) en production
|
||
- **📱 Applications mobiles** Expo 54 (client + admin/livreur)
|
||
- **🏷️ Prix par quantité activables/désactivables** — visibilité client filtrée automatiquement
|
||
- **⏱️ Bouton "Client absent"** — timer 5 min au statut `arrived`, amende automatique appliquée au **client** si absent
|
||
- **📲 OTA updates** canaux nommés par rôle (`pre-prod-client`, `production-client`, `pre-prod-admin`, etc.)
|
||
|
||
### 🏗️ Stack Technique
|
||
|
||
| Composant | Technologie |
|
||
|-----------|-------------|
|
||
| **Backend** | Go 1.24 + Gin · port 8080 |
|
||
| **Base de données** | PostgreSQL 16 |
|
||
| **Cache / Sessions / Queues** | Redis 7 |
|
||
| **Frontend web** | React 19 + TypeScript + Vite (`frontend-prep/`) |
|
||
| **App mobile client** | Expo 54 + React Native (`mobile/`) |
|
||
| **App mobile admin/livreur** | Expo 54 + React Native (`frontend-admin/`) |
|
||
| **WAF / Reverse proxy** | Nginx + ModSecurity OWASP CRS |
|
||
| **Géocodage** | Nominatim (OpenStreetMap) |
|
||
| **Routing / ETA** | TomTom Routing API |
|
||
| **Push notifications** | Expo Push Notifications |
|
||
| **Paiements** | NowPayments (crypto) |
|
||
| **Messagerie** | Telegram Bot API |
|
||
|
||
---
|
||
|
||
### 🏗️ Architecture Globale du Systeme
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Frontend["🖥️ Frontend (React/TypeScript)"]
|
||
UI_CLIENT[Interface Client]
|
||
UI_ADMIN[Interface Admin]
|
||
UI_LIVREUR[Interface Livreur]
|
||
UI_CABINE[Interface Cabine]
|
||
TOMTOM_MAP[TomTomMap Component]
|
||
end
|
||
|
||
subgraph API_GATEWAY["🌐 API Gateway (Go/Gin)"]
|
||
AUTH[Middleware Auth JWT]
|
||
ROUTES[Router]
|
||
end
|
||
|
||
subgraph API_V1["📱 API v1"]
|
||
CLIENT_API["/api/v1/client"]
|
||
LIVREUR_API["/api/v1/livreur"]
|
||
CABINE_API["/api/v1/cabine"]
|
||
PUBLIC_API["/api/v1/public"]
|
||
end
|
||
|
||
subgraph API_V2["👨💼 API v2 Admin"]
|
||
ADMIN_API["/api/v2/admin"]
|
||
end
|
||
|
||
subgraph HANDLERS["⚙️ Handlers"]
|
||
H_AUTH[Auth Handler]
|
||
H_PRODUCT[Product Handler]
|
||
H_PANIER[Panier Handler]
|
||
H_COMMAND[Command Handler]
|
||
H_DELIVERY[Delivery Handler]
|
||
H_GPS[GPS Handler]
|
||
H_ETA[ETA Handler]
|
||
H_PENALTY[Penalty Handler]
|
||
end
|
||
|
||
subgraph SERVICES["🔧 Services"]
|
||
S_GEO[Geo Service]
|
||
S_TOMTOM[TomTom Service]
|
||
S_QUEUE[Queue Service]
|
||
S_NOTIF[Notification Service]
|
||
end
|
||
|
||
subgraph EXTERNAL["🌍 APIs Externes"]
|
||
NOMINATIM[Nominatim API]
|
||
TOMTOM_API[TomTom API]
|
||
end
|
||
|
||
subgraph STORAGE["💾 Stockage"]
|
||
POSTGRES[(PostgreSQL)]
|
||
REDIS[(Redis)]
|
||
end
|
||
|
||
subgraph WORKERS["⚡ Workers"]
|
||
W_ASSIGN[Auto-Assign Worker]
|
||
W_CLEANUP[Cleanup Worker]
|
||
W_ETA[ETA Update Worker]
|
||
end
|
||
|
||
%% Frontend connections
|
||
UI_CLIENT --> AUTH
|
||
UI_ADMIN --> AUTH
|
||
UI_LIVREUR --> AUTH
|
||
UI_CABINE --> AUTH
|
||
TOMTOM_MAP --> TOMTOM_API
|
||
|
||
%% API Gateway
|
||
AUTH --> ROUTES
|
||
ROUTES --> API_V1
|
||
ROUTES --> API_V2
|
||
|
||
%% API to Handlers
|
||
CLIENT_API --> H_AUTH
|
||
CLIENT_API --> H_PANIER
|
||
CLIENT_API --> H_COMMAND
|
||
LIVREUR_API --> H_DELIVERY
|
||
LIVREUR_API --> H_GPS
|
||
CABINE_API --> H_COMMAND
|
||
PUBLIC_API --> H_PRODUCT
|
||
ADMIN_API --> H_COMMAND
|
||
ADMIN_API --> H_DELIVERY
|
||
ADMIN_API --> H_GPS
|
||
ADMIN_API --> H_PENALTY
|
||
|
||
%% Handlers to Services
|
||
H_GPS --> S_GEO
|
||
H_GPS --> S_TOMTOM
|
||
H_ETA --> S_TOMTOM
|
||
H_DELIVERY --> S_QUEUE
|
||
H_COMMAND --> S_NOTIF
|
||
|
||
%% Services to External
|
||
S_GEO --> NOMINATIM
|
||
S_TOMTOM --> TOMTOM_API
|
||
|
||
%% Services to Storage
|
||
S_GEO --> REDIS
|
||
S_QUEUE --> REDIS
|
||
H_COMMAND --> POSTGRES
|
||
H_PRODUCT --> POSTGRES
|
||
H_DELIVERY --> POSTGRES
|
||
|
||
%% Workers
|
||
W_ASSIGN --> S_GEO
|
||
W_ASSIGN --> S_QUEUE
|
||
W_ASSIGN --> POSTGRES
|
||
W_CLEANUP --> REDIS
|
||
W_CLEANUP --> POSTGRES
|
||
W_ETA --> S_TOMTOM
|
||
W_ETA --> REDIS
|
||
```
|
||
|
||
### 🔄 Flux des Donnees par Role
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph Client["👤 Client"]
|
||
C1[Consulter Produits]
|
||
C2[Gerer Panier + Stock auto]
|
||
C3[Passer Commande]
|
||
C4[Suivre Livraison + ETA]
|
||
C5[Approuver/Annuler]
|
||
C6[Notifications Push]
|
||
C7[Solde Parrainage]
|
||
end
|
||
|
||
subgraph Admin["👨💼 Admin"]
|
||
A1[Gerer Produits/Clients]
|
||
A2[Voir/Modifier Commandes]
|
||
A3[Assigner Livreurs GPS]
|
||
A4[Gerer Penalites]
|
||
A5[Surveiller GPS + Queues]
|
||
A6[Parametres Globaux]
|
||
end
|
||
|
||
subgraph Livreur["🚚 Livreur"]
|
||
L1[Voir Mes Livraisons]
|
||
L2[Mettre a Jour Position GPS]
|
||
L3[Changer Statut]
|
||
L4[Voir Ma Queue]
|
||
L5[Alertes Police]
|
||
L6[Notifications Push]
|
||
end
|
||
|
||
subgraph Cabine["🏭 Cabine/Cuisine"]
|
||
K1[Voir Articles Commande]
|
||
K2[Preparer/Marquer Pret]
|
||
K3[Assigner Livreur]
|
||
K4[Gerer Penalites Client]
|
||
K5[Voir Alertes]
|
||
end
|
||
|
||
subgraph Backend["⚙️ Backend"]
|
||
API[API REST]
|
||
GPS[Service GPS]
|
||
QUEUE[Gestion Queue]
|
||
end
|
||
|
||
C1 & C2 & C3 & C4 & C5 & C6 & C7 --> API
|
||
A1 & A2 & A3 & A4 & A5 & A6 --> API
|
||
L1 & L2 & L3 & L4 & L5 & L6 --> API
|
||
K1 & K2 & K3 & K4 & K5 --> API
|
||
API --> GPS
|
||
API --> QUEUE
|
||
```
|
||
|
||
### 📡 Architecture des Endpoints API
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph PUBLIC["🔓 Endpoints Publics (sans auth)"]
|
||
P1["GET /api/v1/products"]
|
||
P2["GET /api/v1/products/:id"]
|
||
P3["GET /api/v1/products/category/:category"]
|
||
P4["GET /api/v1/categories"]
|
||
P5["GET /api/v1/app-settings"]
|
||
P6["POST /api/v1/webhooks/nowpayments"]
|
||
P7["POST /webhook/telegram"]
|
||
end
|
||
|
||
subgraph AUTH_PUBLIC["🔑 Auth Publique (rate-limited)"]
|
||
AP1["POST /api/v1/auth/login"]
|
||
AP2["POST /api/v1/auth/logout"]
|
||
AP3["PUT /api/v1/auth/change-password (JWT)"]
|
||
end
|
||
|
||
subgraph CLIENT_AUTH["🔐 Client (JWT Required)"]
|
||
C1["POST /api/v1/panier/add"]
|
||
C2["GET /api/v1/panier/:username"]
|
||
C3["DELETE /api/v1/panier/remove"]
|
||
C4["DELETE /api/v1/panier/clear"]
|
||
C5["POST /api/v1/checkout"]
|
||
C6["GET /api/v1/my-commands"]
|
||
C7["GET /api/v1/my-commands/history"]
|
||
C8["GET /api/v1/my-commands/history/detailed"]
|
||
C9["GET /api/v1/commands/:id"]
|
||
C10["GET /api/v1/commands/:id/status"]
|
||
C11["GET /api/v1/commands/:id/tracking"]
|
||
C12["GET /api/v1/commands/:id/eta"]
|
||
C13["GET /api/v1/commands/:id/items"]
|
||
C14["GET /api/v1/commands/:id/history"]
|
||
C15["POST /api/v1/commands/:id/approve"]
|
||
C16["POST /api/v1/commands/:id/cancel"]
|
||
C17["POST /api/v1/commands/:id/address/respond"]
|
||
C18["GET /api/v1/my-cancellation-history"]
|
||
C19["GET /api/v1/penalties"]
|
||
C20["GET /api/v1/notifications"]
|
||
C21["POST /api/v1/notifications/read"]
|
||
C22["GET /api/v1/profile"]
|
||
C23["PUT /api/v1/profile/update"]
|
||
C24["GET /api/v1/referral/balance"]
|
||
C25["GET /api/v1/parrain"]
|
||
C26["GET /api/v1/commands/:id/payment-status"]
|
||
C27["POST /api/v1/telegram/link-token"]
|
||
C28["GET /api/v1/telegram/status"]
|
||
C29["DELETE /api/v1/telegram/unlink"]
|
||
end
|
||
|
||
subgraph ADMIN_AUTH["👨💼 Admin v2 (JWT Required)"]
|
||
A1["GET /api/v2/admin/protected/orders"]
|
||
A2["GET /api/v2/admin/protected/orders/:id"]
|
||
A3["PUT /api/v2/admin/protected/orders/:id/status"]
|
||
A4["PUT /api/v2/admin/protected/orders/:id/address"]
|
||
A5["POST /api/v2/admin/protected/orders/:id/notify-client"]
|
||
A6["POST /api/v2/admin/protected/orders/:id/auto-assign"]
|
||
A7["POST /api/v2/admin/protected/orders/auto-assign-all"]
|
||
A8["POST /api/v2/admin/protected/orders/:id/force-validate"]
|
||
A9["POST /api/v2/admin/protected/orders/:id/confirm-reception"]
|
||
A10["GET /api/v2/admin/protected/delivery-persons"]
|
||
A11["GET /api/v2/admin/protected/delivery-persons/:username"]
|
||
A12["GET /api/v2/admin/protected/delivery-persons/:username/location"]
|
||
A13["GET /api/v2/admin/protected/delivery/queues"]
|
||
A14["POST /api/v2/admin/protected/delivery/distances"]
|
||
A15["POST/GET /api/v2/admin/protected/products/:id"]
|
||
A16["POST /api/v2/admin/protected/categories"]
|
||
A17["POST /api/v2/admin/protected/penalty"]
|
||
A18["GET /api/v2/admin/protected/all/clients"]
|
||
A19["PUT /api/v2/admin/protected/clients/:id"]
|
||
A20["GET /api/v2/admin/protected/alerts"]
|
||
A21["GET/PUT /api/v2/admin/protected/settings"]
|
||
A22["GET /api/v2/admin/protected/penalties/all"]
|
||
A23["GET /api/v2/admin/protected/addresses"]
|
||
end
|
||
|
||
subgraph LIVREUR_AUTH["🚚 Livreur (JWT Required)"]
|
||
L1["GET /api/v1/livreur/deliveries"]
|
||
L2["GET /api/v1/livreur/deliveries/:id"]
|
||
L3["POST /api/v1/livreur/deliveries/:id/start"]
|
||
L4["PUT /api/v1/livreur/deliveries/:id/status"]
|
||
L5["GET /api/v1/livreur/deliveries/:id/nav-link"]
|
||
L6["POST /api/v1/livreur/location/update"]
|
||
L7["GET /api/v1/livreur/location"]
|
||
L8["POST /api/v1/livreur/update/status"]
|
||
L9["GET /api/v1/livreur/status"]
|
||
L10["GET /api/v1/livreur/queue"]
|
||
L11["POST /api/v1/livreur/alert"]
|
||
L12["DELETE /api/v1/livreur/alert/:id"]
|
||
L13["GET /api/v1/livreur/alerts"]
|
||
L14["GET /api/v1/livreur/notifications"]
|
||
L15["POST /api/v1/livreur/notifications/read"]
|
||
L16["POST /api/v1/livreur/telegram/link-token"]
|
||
L17["GET /api/v1/livreur/telegram/status"]
|
||
L18["DELETE /api/v1/livreur/telegram/unlink"]
|
||
end
|
||
|
||
subgraph CABINE_AUTH["🏭 Cabine (JWT Required)"]
|
||
K1["GET /api/v1/cabine/commands/:id/items"]
|
||
K2["PUT /api/v1/cabine/commands/:id/status"]
|
||
K3["PUT /api/v1/cabine/items/:item_id/status"]
|
||
K4["POST /api/v1/cabine/commands/:id/confirm-reception"]
|
||
K5["POST /api/v1/cabine/commands/:id/assign"]
|
||
K6["POST /api/v1/cabine/commands/:id/notify-client"]
|
||
K7["GET /api/v1/cabine/all/deliveryman"]
|
||
K8["GET /api/v1/cabine/alerts"]
|
||
K9["GET /api/v1/cabine/penalties/all"]
|
||
K10["GET /api/v1/cabine/addresses"]
|
||
K11["GET /api/v1/cabine/notifications"]
|
||
K12["POST /api/v1/cabine/telegram/link-token"]
|
||
end
|
||
|
||
GW[API Gateway :8080]
|
||
GW --> PUBLIC
|
||
GW --> AUTH_PUBLIC
|
||
GW --> CLIENT_AUTH
|
||
GW --> ADMIN_AUTH
|
||
GW --> LIVREUR_AUTH
|
||
GW --> CABINE_AUTH
|
||
```
|
||
|
||
### 🔐 Flux d'Authentification
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as Utilisateur
|
||
participant F as Frontend
|
||
participant API as Backend API
|
||
participant DB as PostgreSQL
|
||
participant R as Redis
|
||
participant TG as Telegram
|
||
|
||
rect rgb(200, 230, 200)
|
||
Note over U,R: Registration
|
||
U->>F: Remplir formulaire inscription
|
||
F->>API: POST /auth/register
|
||
API->>DB: Creer utilisateur
|
||
DB-->>API: User cree
|
||
API-->>F: 201 Created + User info
|
||
F-->>U: Compte cree
|
||
end
|
||
|
||
rect rgb(200, 200, 230)
|
||
Note over U,TG: Login Standard (2FA desactivee)
|
||
U->>F: Entrer credentials
|
||
F->>API: POST /auth/login
|
||
API->>DB: Verifier credentials
|
||
DB-->>API: User valide
|
||
API->>API: Generer JWT
|
||
API->>R: Stocker session
|
||
API-->>F: 200 OK + { access_token, user }
|
||
F->>F: Stocker token
|
||
F-->>U: Connecte
|
||
end
|
||
|
||
rect rgb(255, 240, 200)
|
||
Note over U,TG: Login avec 2FA (Telegram active)
|
||
U->>F: Entrer credentials
|
||
F->>API: POST /auth/login
|
||
API->>DB: Verifier credentials + verifier 2FA active
|
||
DB-->>API: User valide, 2FA requise
|
||
API->>R: Stocker session_token (TTL 5min)
|
||
API->>TG: Envoyer code 6 chiffres via bot Telegram
|
||
API-->>F: 200 OK + { requires_2fa: true, session_token }
|
||
F-->>U: Afficher saisie du code Telegram
|
||
U->>F: Entrer code recu sur Telegram
|
||
F->>API: POST /auth/2fa/verify { session_token, code }
|
||
API->>R: Verifier code + session_token
|
||
R-->>API: Code valide
|
||
API->>API: Generer JWT
|
||
API->>R: Stocker session
|
||
API-->>F: 200 OK + { access_token, user }
|
||
F->>F: Stocker token
|
||
F-->>U: Connecte
|
||
end
|
||
|
||
rect rgb(230, 200, 200)
|
||
Note over U,R: Requete Authentifiee
|
||
U->>F: Action protegee
|
||
F->>API: Request + Authorization: Bearer JWT
|
||
API->>API: Valider JWT
|
||
API->>R: Verifier session active
|
||
R-->>API: Session valide
|
||
API->>DB: Executer action
|
||
DB-->>API: Resultat
|
||
API-->>F: Response
|
||
F-->>U: Resultat affiche
|
||
end
|
||
```
|
||
|
||
### 📦 Flux Complet d'une Commande
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant F as Frontend
|
||
participant API as Backend
|
||
participant GPS as Service GPS
|
||
participant Q as Queue Service
|
||
participant L as Livreur
|
||
participant DB as PostgreSQL
|
||
participant R as Redis
|
||
|
||
rect rgb(220, 240, 220)
|
||
Note over C,R: 1. Ajout au panier
|
||
C->>F: Ajouter produit
|
||
F->>API: POST /panier/add {product_id, quantity}
|
||
API->>DB: Verifier stock (GetProductStockByID)
|
||
DB-->>API: Stock OK
|
||
API->>DB: Decrementer stock (DecrementProductStockByID)
|
||
API->>DB: Ajouter ligne panier
|
||
API-->>F: 201 Created
|
||
F-->>C: Panier mis a jour
|
||
Note over C,R: Si suppression panier → stock restitue automatiquement
|
||
end
|
||
|
||
rect rgb(230, 245, 230)
|
||
Note over C,R: 2. Checkout
|
||
C->>F: Valider panier
|
||
F->>API: POST /checkout {address}
|
||
API->>GPS: Geocoder adresse
|
||
GPS->>R: Check cache
|
||
R-->>GPS: Miss
|
||
GPS->>GPS: Appel Nominatim
|
||
GPS->>R: Cache resultat (7j)
|
||
GPS-->>API: Coordonnees
|
||
API->>DB: Creer commande
|
||
API->>Q: Chercher livreur proche
|
||
Q->>R: Get positions livreurs
|
||
Q->>Q: Calcul distances Haversine
|
||
Q->>GPS: Calculer ETA (TomTom)
|
||
Q-->>API: Livreur assigne
|
||
API->>DB: Update commande
|
||
API->>R: Ajouter a queue livreur
|
||
API-->>F: Commande creee + ETA
|
||
F-->>C: Confirmation
|
||
end
|
||
|
||
rect rgb(230, 230, 245)
|
||
Note over C,R: 3. Livraison
|
||
L->>API: GET /livreur/deliveries
|
||
API-->>L: Liste commandes
|
||
L->>API: POST /deliveries/:id/start
|
||
L->>API: POST /location/update {lat, lng}
|
||
API->>R: Update position
|
||
API->>R: Publish update
|
||
R-->>F: Notification position
|
||
F-->>C: Carte mise a jour
|
||
L->>API: PUT /deliveries/:id/status {en_route}
|
||
Note over L,API: ETA calcule et stocke dans Redis
|
||
L->>API: PUT /deliveries/:id/status {arrived}
|
||
L->>API: PUT /deliveries/:id/status {livre}
|
||
end
|
||
|
||
rect rgb(245, 230, 230)
|
||
Note over C,R: 4. Finalisation
|
||
C->>F: Approuver livraison
|
||
F->>API: POST /commands/:id/approve {rating}
|
||
API->>DB: Update statut approved
|
||
API->>R: Retirer de queue
|
||
API-->>F: Confirmation
|
||
F-->>C: Livraison terminee
|
||
end
|
||
```
|
||
|
||
### 💾 Schema de la Base de Donnees
|
||
|
||
```mermaid
|
||
erDiagram
|
||
CLIENTS {
|
||
int id PK
|
||
string username UK
|
||
string password
|
||
string nom
|
||
string prenom
|
||
string telephone UK
|
||
int command
|
||
int point
|
||
int points_zipette
|
||
float amende
|
||
int cancellations_count
|
||
timestamp created_at
|
||
}
|
||
|
||
ADMINS {
|
||
int id PK
|
||
string username UK
|
||
string password
|
||
string role
|
||
timestamp created_at
|
||
}
|
||
|
||
LIVREURS {
|
||
int id PK
|
||
string username UK
|
||
string password
|
||
string status
|
||
timestamp created_at
|
||
}
|
||
|
||
CABINES {
|
||
int id PK
|
||
string username UK
|
||
string password
|
||
timestamp created_at
|
||
}
|
||
|
||
PRODUCTS {
|
||
int id PK
|
||
string nom
|
||
string description
|
||
string category
|
||
int stock
|
||
float prix
|
||
timestamp created_at
|
||
}
|
||
|
||
PRODUCT_PRICES {
|
||
int id PK
|
||
int product_id FK
|
||
int quantity
|
||
float price
|
||
boolean active_price
|
||
}
|
||
|
||
PRODUCT_MEDIA {
|
||
int id PK
|
||
int product_id FK
|
||
string type
|
||
string url
|
||
}
|
||
|
||
PANIER {
|
||
int id PK
|
||
string username FK
|
||
string name_product
|
||
string category
|
||
int quantity
|
||
float price
|
||
timestamp created_at
|
||
}
|
||
|
||
COMMANDS {
|
||
int id PK
|
||
string username FK
|
||
string status
|
||
float total
|
||
string adresse
|
||
float dest_latitude
|
||
float dest_longitude
|
||
string livreur_assign FK
|
||
timestamp created_at
|
||
timestamp updated_at
|
||
}
|
||
|
||
COMMAND_ITEMS {
|
||
int id PK
|
||
int command_id FK
|
||
string produit
|
||
int quantite
|
||
float prix
|
||
string status
|
||
string category
|
||
}
|
||
|
||
RATINGS {
|
||
int id PK
|
||
int command_id FK
|
||
string deliveryman
|
||
int rating
|
||
string comment
|
||
timestamp created_at
|
||
}
|
||
|
||
CLIENTS ||--o{ PANIER : "possede"
|
||
CLIENTS ||--o{ COMMANDS : "passe"
|
||
PRODUCTS ||--o{ PRODUCT_PRICES : "a"
|
||
PRODUCTS ||--o{ PRODUCT_MEDIA : "a"
|
||
COMMANDS ||--o{ COMMAND_ITEMS : "contient"
|
||
COMMANDS ||--o| RATINGS : "a"
|
||
LIVREURS ||--o{ COMMANDS : "livre"
|
||
```
|
||
|
||
### 🗄️ Structure Redis
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph Sessions["🔐 Sessions"]
|
||
S1["session:{token}<br/>TTL: 5h (client) / 2h (admin)"]
|
||
end
|
||
|
||
subgraph Positions["📍 Positions GPS"]
|
||
P1["delivery:location:{username}<br/>TTL: 2h"]
|
||
P2["delivery:status:{username}<br/>TTL: 1h"]
|
||
end
|
||
|
||
subgraph Queues["📋 Queues Livraison"]
|
||
Q1["livreur:queue:{username}<br/>List of command_ids"]
|
||
Q2["queue:size:{username}<br/>Integer"]
|
||
end
|
||
|
||
subgraph Cache["💾 Cache"]
|
||
C1["geocode:cache:{hash}<br/>TTL: 7j"]
|
||
C2["command:destination:{id}<br/>TTL: 4h"]
|
||
C3["product:cache:{id}<br/>TTL: 1h"]
|
||
end
|
||
|
||
subgraph Notifs["🔔 Notifications"]
|
||
N1["notifications:{username}<br/>TTL: 1h"]
|
||
end
|
||
|
||
subgraph PubSub["�� Pub/Sub Channels"]
|
||
PS1["channel:position_updates"]
|
||
PS2["channel:order_status"]
|
||
PS3["channel:notifications"]
|
||
end
|
||
|
||
REDIS[(Redis Server)]
|
||
REDIS --- Sessions
|
||
REDIS --- Positions
|
||
REDIS --- Queues
|
||
REDIS --- Cache
|
||
REDIS --- Notifs
|
||
REDIS --- PubSub
|
||
```
|
||
|
||
---
|
||
|
||
## 🖥️ Infrastructure Serveurs
|
||
|
||
### Réseau VPN (WireGuard)
|
||
|
||
Tous les serveurs backend communiquent via un réseau WireGuard privé `10.0.0.0/24`. Le SSH est restreint à l'IP VPN uniquement sur les serveurs sensibles — il faut être connecté au VPN pour s'y connecter.
|
||
|
||
| Serveur | IP Publique | IP VPN | Rôle |
|
||
|---------|-------------|--------|------|
|
||
| **vpn-uber** | `45.150.111.158` | `10.0.0.1` | Serveur WireGuard — point d'entrée VPN et jump host SSH |
|
||
| **monitoring-uber** | `185.103.167.138` | `10.0.0.2` | Wazuh · Dozzle · Beszel hub · SSH via VPN uniquement |
|
||
| **backup-mln** | `85.121.176.241` | `10.0.0.4` | MinIO S3 · ClamAV · Beszel agent · SSH via VPN uniquement |
|
||
| **bdd-redis-prod** | `132.243.162.62` | `10.0.0.5` | PostgreSQL 16 · Redis 7 · Beszel agent · SSH via VPN uniquement |
|
||
| **prod-uber** | `185.103.166.119` | `10.0.0.6` | Backend Go + WAF nginx · accessible publiquement sur 80/443 |
|
||
| **pre-prod-uber** | `185.103.166.112` | — | Environnement de pré-production |
|
||
| **s3-uber** | `80.96.58.164` | — | Stockage S3 externe · Dozzle agent |
|
||
|
||
### Architecture DMZ / LAN
|
||
|
||
```mermaid
|
||
graph TB
|
||
Internet((Internet)) -->|"80/443 public"| Prod["prod-uber — DMZ<br/>185.103.166.119"]
|
||
Prod -->|"VPN (10.0.0.6)"| BDD["bdd-redis-prod — LAN<br/>10.0.0.5"]
|
||
BDD --> PG["PostgreSQL :5432"]
|
||
BDD --> Redis["Redis :6379"]
|
||
|
||
VPN["vpn-uber — 10.0.0.1<br/>jump host SSH"] -.->|"accès admin uniquement"| BDD
|
||
VPN -.-> Prod
|
||
|
||
style Internet fill:#eee,stroke:#999
|
||
style Prod fill:#fde68a,stroke:#b45309
|
||
style BDD fill:#bbf7d0,stroke:#15803d
|
||
style VPN fill:#bfdbfe,stroke:#1d4ed8
|
||
```
|
||
|
||
L'application prod-uber se connecte à PostgreSQL et Redis via les IP VPN :
|
||
- `DB_HOST=10.0.0.5` (PostgreSQL sur bdd-redis-prod)
|
||
- `REDIS_HOST=10.0.0.5` (Redis sur bdd-redis-prod)
|
||
|
||
### Monitoring
|
||
|
||
Toutes les ressources monitoring sont accessibles via VPN (`10.0.0.2`) :
|
||
|
||
| Outil | URL | Description |
|
||
|-------|-----|-------------|
|
||
| **Wazuh** | `https://10.0.0.2` | SIEM — alertes sécurité, logs agents |
|
||
| **Dozzle** | `https://10.0.0.2:8080` | Logs Docker de tous les serveurs en temps réel |
|
||
| **Beszel** | `https://10.0.0.2:8090` | Métriques système (CPU, RAM, disque, réseau) |
|
||
|
||
Dozzle agrège les logs de : `pre-prod-uber`, `prod-uber`, `backup-mln (VPN)`, `s3-uber`.
|
||
Beszel surveille : `monitoring-uber`, `backup-mln` (agent Docker port 10001), `bdd-redis-prod` (agent binaire systemd port 10001).
|
||
|
||
Nettoyage automatique des logs Wazuh : cron tous les dimanches à 3h00 sur monitoring-uber (`/usr/local/bin/clean-wazuh-logs.sh`).
|
||
|
||
### Sécurité réseau
|
||
|
||
- **UFW** activé sur monitoring-uber et backup-mln : SSH bloqué depuis IP publique, accessible uniquement via VPN
|
||
- **UFW** activé sur bdd-redis-prod : SSH, PostgreSQL et Redis accessibles uniquement depuis le réseau VPN (`10.0.0.0/24`)
|
||
- Ports Docker liés à l'IP VPN (`10.0.0.4:port:port`) pour ne pas bypasser UFW
|
||
- **ClamAV** sur backup-mln : scan antivirus quotidien de `/mnt/data`
|
||
|
||
### Accès SSH aux serveurs VPN-only
|
||
|
||
```bash
|
||
# Via le jump host vpn-uber
|
||
ssh -J root@45.150.111.158 root@10.0.0.2 # monitoring-uber
|
||
ssh -J root@45.150.111.158 root@10.0.0.4 # backup-mln
|
||
ssh -J root@45.150.111.158 root@10.0.0.5 # bdd-redis-prod
|
||
```
|
||
|
||
### Sauvegarde S3 (backup-mln)
|
||
|
||
MinIO S3 tourne sur backup-mln avec nginx SSL proxy :
|
||
|
||
| Endpoint | Adresse |
|
||
|----------|---------|
|
||
| API S3 | `https://10.0.0.4:9000` (via VPN) |
|
||
| Console MinIO | `https://10.0.0.4:9001` (via VPN) |
|
||
| Console nginx | `https://10.0.0.4:8080` (via VPN) |
|
||
|
||
Les données sont montées sur `/mnt/data`.
|
||
|
||
---
|
||
|
||
## 🚀 Déploiement Production
|
||
|
||
### Prérequis
|
||
|
||
- Docker + Docker Compose
|
||
- Certificats SSL : `fullchain.pem` + `privkey.pem` (Let's Encrypt recommandé)
|
||
- Fichier `.env` complété (voir `.env.example`)
|
||
|
||
### Structure des fichiers
|
||
|
||
```
|
||
docker/
|
||
├── backend/
|
||
│ ├── Dockerfile # Stage builder (Go), runtime, waf (nginx+ModSec)
|
||
│ ├── nginx.conf # Config nginx hardened (TLS 1.2/1.3, HSTS, CSP)
|
||
│ ├── custom-rules.conf # Règles ModSecurity personnalisées
|
||
│ └── entrypoint.sh # Entrypoint backend Go
|
||
├── frontend/
|
||
│ ├── Dockerfile # Build React/Vite + nginx interne
|
||
│ └── nginx.conf # Nginx interne (HTTP, sans TLS)
|
||
├── certs/ # Certificats SSL (non versionnés)
|
||
│ ├── fullchain.pem
|
||
│ └── privkey.pem
|
||
└── docker-compose-prod.yml
|
||
```
|
||
|
||
### Déploiement
|
||
|
||
```bash
|
||
# 1. Copier les certificats
|
||
mkdir -p docker/certs
|
||
cp /etc/letsencrypt/live/votre-domaine/fullchain.pem docker/certs/
|
||
cp /etc/letsencrypt/live/votre-domaine/privkey.pem docker/certs/
|
||
chmod 644 docker/certs/privkey.pem
|
||
|
||
# 2. Configurer les variables d'environnement
|
||
cp docker/.env.example docker/.env
|
||
# Éditer docker/.env avec vos valeurs
|
||
|
||
# 3. Lancer la stack
|
||
docker compose -f docker/docker-compose-prod.yml up -d --build
|
||
|
||
# 4. Vérifier les logs
|
||
docker compose -f docker/docker-compose-prod.yml logs -f waf
|
||
```
|
||
|
||
### Services Docker (prod-uber)
|
||
|
||
| Service | Image | Rôle |
|
||
|---------|-------|------|
|
||
| `waf` | owasp/modsecurity-crs:nginx-alpine | Point d'entrée HTTPS (ports 80/443) |
|
||
| `backend` | Go 1.24 alpine | API REST (port 8080 interne) |
|
||
| `frontend` | nginx:alpine | SPA React (port 80 interne) |
|
||
|
||
> PostgreSQL et Redis **ne tournent plus sur prod-uber**. Ils sont hébergés sur le serveur dédié `bdd-redis-prod` (`10.0.0.5`) et accessibles via le VPN WireGuard. Voir la section [Infrastructure Serveurs](#-infrastructure-serveurs).
|
||
|
||
### Variables d'environnement requises
|
||
|
||
```bash
|
||
# Base de données (bdd-redis-prod via VPN)
|
||
DB_HOST=10.0.0.5 # IP VPN de bdd-redis-prod
|
||
DB_PORT=5432
|
||
DB_PASSWORD= # Mot de passe PostgreSQL
|
||
|
||
# Redis (bdd-redis-prod via VPN)
|
||
REDIS_HOST=10.0.0.5 # IP VPN de bdd-redis-prod
|
||
REDIS_PORT=6379
|
||
REDIS_PASSWORD= # Mot de passe Redis
|
||
|
||
# JWT
|
||
USER_JWT_SECRET= # Secret JWT clients (min 32 chars)
|
||
ADMIN_JWT_SECRET= # Secret JWT admin/livreur/cabine (min 32 chars)
|
||
|
||
# TomTom (rotation automatique entre les 3 clés)
|
||
TOMTOM_API_KEY= # Clé TomTom principale / legacy
|
||
TOMTOM_API_KEY_1= # Clé TomTom #1
|
||
TOMTOM_API_KEY_2= # Clé TomTom #2
|
||
TOMTOM_API_KEY_3= # Clé TomTom #3
|
||
|
||
# Divers
|
||
SESSION_SECRET= # Secret sessions
|
||
TELEGRAM_WEBHOOK_URL= # URL webhook Telegram
|
||
TELEGRAM_WEBHOOK_SECRET= # Secret webhook Telegram
|
||
NOWPAYMENTS_IPN_SECRET= # Secret IPN NowPayments
|
||
|
||
# Stockage médias produits (photos/vidéos)
|
||
STORAGE_DRIVER=local # "local" (disque, défaut) ou "s3" (RustFS)
|
||
# Requis uniquement si STORAGE_DRIVER=s3 :
|
||
S3_REGION= # Région S3 (arbitraire pour RustFS, ex: us-east-1)
|
||
S3_BUCKET= # Nom du bucket
|
||
S3_ENDPOINT= # URL du endpoint S3-compatible (RustFS, IP VPN interne)
|
||
RUSTFS_ACCESS_KEY= # Clé d'accès RustFS
|
||
RUSTFS_SECRET_KEY= # Clé secrète RustFS
|
||
```
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Upload["CreateProduct /<br/>UploadMedia"] --> Check{"STORAGE_DRIVER"}
|
||
Check -->|"local (défaut)"| Local["LocalStorage<br/>disque ./uploads"]
|
||
Check -->|"s3"| S3["S3Storage<br/>RustFS (auto-hébergé)"]
|
||
|
||
Delete["DeleteProduct /<br/>DeleteMedia"] --> KeyCheck{"media.Key<br/>rempli ?"}
|
||
KeyCheck -->|non| Local
|
||
KeyCheck -->|oui| S3
|
||
```
|
||
|
||
---
|
||
|
||
## 🔔 Notifications
|
||
|
||
Les notifications clients et livreurs sont gérées **exclusivement via Telegram** — les push notifications Expo (iOS/Android) ne sont plus utilisées.
|
||
|
||
### Notifications Telegram
|
||
|
||
Clients, livreurs et admins reçoivent leurs alertes via un bot Telegram lié à leur compte.
|
||
|
||
**Types de notifications envoyées :**
|
||
- `assigned` — Commande assignée à un livreur
|
||
- `en_route` — Livreur en route (avec ETA)
|
||
- `arrived` — Livreur arrivé
|
||
- `livre` — Commande livrée
|
||
- `ready_pickup` — Cabine : "descendez chercher votre commande"
|
||
- `address_proposal` — Proposition de changement d'adresse
|
||
|
||
Les admins et livreurs peuvent lier leur compte Telegram pour recevoir des alertes.
|
||
|
||
#### Générer un token de liaison (Admin)
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/telegram/link-token
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse:**
|
||
```json
|
||
{
|
||
"link_token": "abc123xyz",
|
||
"expires_in": 300,
|
||
"bot_url": "https://t.me/votre_bot?start=abc123xyz"
|
||
}
|
||
```
|
||
|
||
#### Générer un token de liaison (Livreur)
|
||
|
||
```bash
|
||
POST /api/v1/livreur/telegram/link-token
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
#### Statut Telegram (Livreur)
|
||
|
||
```bash
|
||
GET /api/v1/livreur/telegram/status
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
#### Délier Telegram
|
||
|
||
```bash
|
||
DELETE /api/v1/livreur/telegram/unlink
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
---
|
||
|
||
## 💸 Paiements Crypto
|
||
|
||
Le système intègre NowPayments pour les paiements en cryptomonnaie.
|
||
|
||
### Webhook IPN (Instant Payment Notification)
|
||
|
||
Endpoint public sécurisé par signature HMAC-SHA512.
|
||
|
||
```bash
|
||
POST /api/v1/webhooks/nowpayments
|
||
Content-Type: application/json
|
||
x-nowpayments-sig: <hmac_sha512_signature>
|
||
```
|
||
|
||
Le webhook met automatiquement à jour le statut de paiement de la commande correspondante.
|
||
|
||
#### Vérifier le statut de paiement d'une commande
|
||
|
||
```bash
|
||
GET /api/v1/commands/:id/payment-status
|
||
Authorization: Bearer <client_token>
|
||
```
|
||
|
||
**Réponse:**
|
||
```json
|
||
{
|
||
"command_id": 1234,
|
||
"payment_status": "confirmed",
|
||
"amount_paid": 25.00,
|
||
"currency": "USDT"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔐 Authentication
|
||
|
||
### Création de compte client
|
||
|
||
Les comptes clients sont créés **uniquement par un administrateur** via `POST /api/v2/admin/protected/clients`. Il n'existe pas d'endpoint d'auto-inscription.
|
||
|
||
---
|
||
|
||
### Login Client
|
||
|
||
**Se connecter et obtenir un token JWT**
|
||
|
||
```bash
|
||
POST /api/v1/auth/login
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"username": "jean_dupont",
|
||
"password": "SecurePass123!"
|
||
}
|
||
```
|
||
|
||
**Réponse standard (200 OK) — 2FA désactivée:**
|
||
```json
|
||
{
|
||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 18000,
|
||
"user": {
|
||
"username": "jean_dupont",
|
||
"role": "client"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Réponse avec 2FA (200 OK) — quand 2FA activée par le client ET Telegram lié ET admin l'a activée:**
|
||
```json
|
||
{
|
||
"requires_2fa": true,
|
||
"session_token": "550e8400-e29b-41d4-a716-446655440000"
|
||
}
|
||
```
|
||
> Un code à 6 chiffres est envoyé automatiquement sur le compte Telegram lié. Le `session_token` expire dans **5 minutes**.
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Données manquantes
|
||
- `401` - Identifiants incorrects
|
||
|
||
---
|
||
|
||
### Vérifier le code 2FA
|
||
|
||
**Valider le code Telegram reçu pour finaliser la connexion**
|
||
|
||
```bash
|
||
POST /api/v1/auth/2fa/verify
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"session_token": "550e8400-e29b-41d4-a716-446655440000",
|
||
"code": "483721"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 18000,
|
||
"user": {
|
||
"username": "jean_dupont",
|
||
"role": "client"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - `session_token` ou `code` manquant
|
||
- `401` - Code incorrect ou session expirée (TTL 5 min)
|
||
- `429` - Trop de tentatives (rate limiting)
|
||
|
||
---
|
||
|
||
### Statut 2FA du compte
|
||
|
||
**Obtenir l'état de la 2FA pour le compte connecté**
|
||
|
||
```bash
|
||
GET /api/v1/two-fa/status
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"two_fa_enabled": true,
|
||
"telegram_linked": true,
|
||
"admin_2fa_enabled": true
|
||
}
|
||
```
|
||
|
||
| Champ | Description |
|
||
|-------|-------------|
|
||
| `two_fa_enabled` | La 2FA est activée sur ce compte client |
|
||
| `telegram_linked` | Un compte Telegram est lié (prérequis pour activer) |
|
||
| `admin_2fa_enabled` | L'admin a activé la 2FA dans les paramètres globaux |
|
||
|
||
---
|
||
|
||
### Activer / Désactiver la 2FA
|
||
|
||
**Basculer l'état de la 2FA sur son propre compte**
|
||
|
||
```bash
|
||
POST /api/v1/two-fa/toggle
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"enabled": true
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"two_fa_enabled": true
|
||
}
|
||
```
|
||
|
||
**Prérequis pour activer (`"enabled": true`):**
|
||
1. Le client doit avoir lié son compte Telegram (via `/api/v1/auth/link-telegram`)
|
||
2. L'administrateur doit avoir activé `telegram_2fa_enabled` dans les paramètres globaux
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Telegram non lié (impossible d'activer sans compte Telegram)
|
||
- `403` - La 2FA n'est pas autorisée par l'administrateur
|
||
- `401` - Non authentifié
|
||
|
||
> **Interface utilisateur:** Sur le frontend web (page profil) et l'app mobile, un toggle permet d'activer/désactiver la 2FA directement depuis les paramètres du compte.
|
||
|
||
---
|
||
|
||
### Logout Client
|
||
|
||
**Se déconnecter (invalider le token)**
|
||
|
||
```bash
|
||
POST /api/v1/auth/logout
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Déconnexion réussie"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🛒 API Client (v1)
|
||
|
||
**Toutes les routes clients nécessitent le header:**
|
||
```
|
||
Authorization: Bearer <client_token>
|
||
```
|
||
|
||
### Produits (Public - Pas d'auth requis)
|
||
|
||
> **Filtrage des prix inactifs :** Chaque prix d'un produit porte un flag `active_price` (booléen). Les endpoints publics et client **n'exposent que les prix actifs** (`active_price = true`). Les rôles `admin` et `cabine` reçoivent tous les prix (actifs et inactifs) pour permettre la gestion complète. Côté frontend (web + mobile), les prix sont également filtrés au chargement (`filter(p => p.active_price !== false)`).
|
||
|
||
#### Liste des produits
|
||
|
||
```bash
|
||
GET /api/v1/products
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"products": [
|
||
{
|
||
"id": 1,
|
||
"nom": "Pizza Margherita",
|
||
"description": "Pizza traditionnelle avec tomate, mozzarella et basilic",
|
||
"category": "pizza",
|
||
"stock": 50,
|
||
"prix": 12.50,
|
||
"prices": [
|
||
{
|
||
"id": 1,
|
||
"product_id": 1,
|
||
"quantity": 1,
|
||
"price": 12.50,
|
||
"active_price": true
|
||
}
|
||
],
|
||
"media": [
|
||
{
|
||
"id": 1,
|
||
"product_id": 1,
|
||
"type": "image",
|
||
"url": "/uploads/pizza_margherita.jpg"
|
||
}
|
||
],
|
||
"created_at": "2025-01-18T00:00:00Z"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Produit par ID
|
||
|
||
```bash
|
||
GET /api/v1/products/:id
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"product": {
|
||
"id": 1,
|
||
"nom": "Pizza Margherita",
|
||
"description": "Pizza traditionnelle avec tomate, mozzarella et basilic",
|
||
"category": "pizza",
|
||
"stock": 50,
|
||
"prix": 12.50,
|
||
"prices": [...],
|
||
"media": [...],
|
||
"created_at": "2025-01-18T00:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `404` - Produit non trouvé
|
||
|
||
---
|
||
|
||
### Gestion du Panier
|
||
|
||
> **Note stock :** Le stock est décrémenté dès l'ajout au panier et restitué automatiquement lors de la suppression d'un article ou du vidage du panier.
|
||
|
||
#### Ajouter au panier
|
||
|
||
```bash
|
||
POST /api/v1/panier/add
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"name_product": "Pizza Margherita",
|
||
"category": "pizza",
|
||
"quantity": 2
|
||
}
|
||
```
|
||
|
||
**Réponse (201 Created):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Produit ajouté au panier avec succès",
|
||
"panier": {
|
||
"id": 15,
|
||
"username": "jean_dupont",
|
||
"name_product": "Pizza Margherita",
|
||
"category": "pizza",
|
||
"quantity": 2,
|
||
"price": 25.00,
|
||
"created_at": "2025-01-18T14:35:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Données invalides ou stock insuffisant
|
||
- `401` - Non authentifié
|
||
- `404` - Produit non trouvé
|
||
|
||
---
|
||
|
||
#### Voir mon panier
|
||
|
||
```bash
|
||
GET /api/v1/panier/:username
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Panier récupéré avec succès",
|
||
"panier": [
|
||
{
|
||
"id": 15,
|
||
"username": "jean_dupont",
|
||
"name_product": "Pizza Margherita",
|
||
"category": "pizza",
|
||
"quantity": 2,
|
||
"price": 25.00,
|
||
"created_at": "2025-01-18T14:35:00Z"
|
||
}
|
||
],
|
||
"count": 1,
|
||
"total_amount": 25.00
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `403` - Accès au panier d'un autre utilisateur
|
||
- `404` - Utilisateur non trouvé
|
||
|
||
---
|
||
|
||
#### Supprimer du panier
|
||
|
||
```bash
|
||
DELETE /api/v1/panier/remove
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"id": 15
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Produit supprimé du panier avec succès",
|
||
"item_id": 15
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `403` - Tentative de supprimer l'article d'un autre utilisateur
|
||
- `404` - Article non trouvé
|
||
|
||
---
|
||
|
||
#### Vider le panier
|
||
|
||
```bash
|
||
DELETE /api/v1/panier/clear
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Panier vidé avec succès"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Commandes
|
||
|
||
#### Valider le panier (Checkout avec Auto-assignation)
|
||
|
||
```bash
|
||
POST /api/v1/checkout
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"delivery_address": "15 Rue de la Paix, 75002 Paris, France"
|
||
}
|
||
```
|
||
|
||
**Réponse avec auto-assignation (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Commande créée et livreur assigné automatiquement",
|
||
"command_id": 1234,
|
||
"delivery_address": "15 Rue de la Paix, 75002 Paris, France",
|
||
"status": "assigned",
|
||
"auto_assigned": true,
|
||
"assigned_to": {
|
||
"username": "john_deliveryman",
|
||
"distance_km": 1.5,
|
||
"travel_time": 8
|
||
}
|
||
}
|
||
```
|
||
|
||
**Réponse sans auto-assignation (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Commande créée - En attente d'assignation",
|
||
"command_id": 1234,
|
||
"delivery_address": "15 Rue de la Paix, 75002 Paris, France",
|
||
"status": "pending",
|
||
"auto_assigned": false
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Panier vide ou adresse manquante
|
||
- `401` - Non authentifié
|
||
- `500` - Erreur création commande
|
||
|
||
**Réponse — adresse connue pour être mal formée (400 Bad Request):**
|
||
|
||
Si l'adresse saisie correspond (match exact ou normalisé — accents/casse/espaces ignorés) à une entrée de la table de corrections gérée par l'admin/cabine (`POST/DELETE /addresses`), le checkout est volontairement bloqué pour forcer une reconfirmation du client plutôt que d'appliquer la correction en silence :
|
||
```json
|
||
{
|
||
"error": "Adresse non reconnue",
|
||
"corrected_address": "15 Rue de la Paix, 75002 Paris, France"
|
||
}
|
||
```
|
||
Le client doit renvoyer la requête avec `delivery_address` = `corrected_address` pour valider le checkout.
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant API as API (ValidateBasket)
|
||
participant DB as adresse_correction
|
||
|
||
C->>API: POST /checkout {delivery_address}
|
||
API->>DB: CheckAddress(delivery_address)
|
||
DB->>DB: Match exact, sinon fallback normalisé<br/>(accents/casse/espaces)
|
||
alt Correction trouvée
|
||
DB-->>API: corrected_address
|
||
API-->>C: 400 {error, corrected_address}
|
||
C->>C: Affiche la suggestion (modal)
|
||
C->>API: POST /checkout {delivery_address: corrected_address}
|
||
API-->>C: 200 Commande créée
|
||
else Aucune correction connue
|
||
DB-->>API: nil
|
||
API-->>C: 200 Commande créée
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
#### Mes commandes avec suivi
|
||
|
||
```bash
|
||
GET /api/v1/my-commands
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"commands": [
|
||
{
|
||
"id": 1234,
|
||
"username": "jean_dupont",
|
||
"status": "assigned",
|
||
"total": 25.00,
|
||
"delivery_address": "15 Rue de la Paix, 75002 Paris",
|
||
"livreur_assign": "john_deliveryman",
|
||
"created_at": "2025-01-18T14:40:00Z",
|
||
"updated_at": "2025-01-18T14:42:00Z",
|
||
"tracking": {
|
||
"eta_minutes": 25,
|
||
"estimated_arrival": "15:05",
|
||
"deliveryman_status": "en_route",
|
||
"queue_position": 2
|
||
}
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Statut temps réel d'une commande
|
||
|
||
```bash
|
||
GET /api/v1/commands/:id/status
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"command_id": 1234,
|
||
"status": "en_route",
|
||
"deliveryman": "john_deliveryman",
|
||
"updated_at": "2025-01-18T15:20:00Z"
|
||
}
|
||
```
|
||
|
||
**Statuts possibles:**
|
||
|
||
| Statut | Description | Qui peut le définir |
|
||
|--------|-------------|---------------------|
|
||
| `pending` | En attente d'assignation | Système (checkout) |
|
||
| `assigned` | Assignée à un livreur | Système (auto-assign), Admin |
|
||
| `en_route` | Livreur en chemin | Livreur, Admin |
|
||
| `arrived` | Livreur arrivé à destination | Livreur, Admin, Cabine (bouton "Le livreur est là") |
|
||
| `livre` | Livrée, en attente d'approbation | Livreur (validation GPS), Admin |
|
||
| `approved` | Approuvée et finalisée | Client (approve), Admin, Cabine |
|
||
| `cancelled` | Annulée | Client, Livreur, Admin |
|
||
|
||
---
|
||
|
||
#### Suivi détaillé avec ETA
|
||
|
||
```bash
|
||
GET /api/v1/commands/:id/tracking
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"command_id": 1234,
|
||
"status": "en_route",
|
||
"deliveryman": "john_deliveryman",
|
||
"eta": {
|
||
"minutes": 22,
|
||
"estimated_arrival": "15:02",
|
||
"arrival_time_unix": 1705669320,
|
||
"updated_at": "2025-01-18T14:40:00Z"
|
||
},
|
||
"queue_position": 2,
|
||
"total_queue_size": 5
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### ETA de livraison
|
||
|
||
```bash
|
||
GET /api/v1/commands/:id/eta
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"command_id": 1234,
|
||
"eta_minutes": 22,
|
||
"estimated_arrival": "15:02:00",
|
||
"queue_position": 2,
|
||
"updated_at": "2025-01-18T14:40:00Z"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Approuver une livraison
|
||
|
||
```bash
|
||
POST /api/v1/commands/:id/approve
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"rating": 5,
|
||
"comment": "Excellent service, livreur très professionnel !"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Livraison approuvée avec succès",
|
||
"command_id": 1234,
|
||
"status": "approved",
|
||
"rating": {
|
||
"deliveryman": "john_deliveryman",
|
||
"rating": 5,
|
||
"comment": "Excellent service, livreur très professionnel !",
|
||
"created_at": "2025-01-18T15:05:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Annuler une commande
|
||
|
||
```bash
|
||
POST /api/v1/commands/:id/cancel
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"reason": "Changement de plans"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Commande annulée avec succès",
|
||
"command_id": 1234,
|
||
"penalty_applied": true,
|
||
"penalty_details": {
|
||
"cancellations_count": 1,
|
||
"penalty_amount": 20.0,
|
||
"warning": "Pénalité appliquée pour annulation"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Profil et Historique
|
||
|
||
#### Mettre à jour mon profil
|
||
|
||
```bash
|
||
PUT /api/v1/profile/update
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"nom": "Nouveau Nom",
|
||
"prenom": "Nouveau Prénom",
|
||
"telephone": "+33687654321",
|
||
"password": "NewSecurePass456!"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Profil mis à jour avec succès",
|
||
"client": {
|
||
"id": 42,
|
||
"username": "jean_dupont",
|
||
"nom": "Nouveau Nom",
|
||
"prenom": "Nouveau Prénom",
|
||
"telephone": "+33687654321",
|
||
"updated_at": "2025-01-18T15:30:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Mes pénalités
|
||
|
||
```bash
|
||
GET /api/v1/penalties
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"client": {
|
||
"username": "jean_dupont",
|
||
"amende": 20.0,
|
||
"cancellations_count": 1,
|
||
"last_penalty_reason": "Annulation tardive"
|
||
},
|
||
"penalty_scale": {
|
||
"1st": 20.0,
|
||
"2nd": 50.0,
|
||
"3rd": 100.0,
|
||
"4th+": 150.0
|
||
},
|
||
"next_penalty": 50.0,
|
||
"warning": "La prochaine annulation entraînera une pénalité de 50 points"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 👨💼 API Admin (v2)
|
||
|
||
**Toutes les routes admin nécessitent le header:**
|
||
```
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
### Authentication Admin
|
||
|
||
#### Login Admin
|
||
|
||
```bash
|
||
POST /api/v2/admin/auth/login
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"username": "admin_master",
|
||
"password": "AdminSecure123!"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 7200,
|
||
"user": {
|
||
"username": "admin_master",
|
||
"role": "admin"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Gestion des Commandes
|
||
|
||
#### Liste des commandes
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/orders
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"orders": [
|
||
{
|
||
"id": 1234,
|
||
"username": "jean_dupont",
|
||
"status": "assigned",
|
||
"total": 25.00,
|
||
"delivery_address": "15 Rue de la Paix, 75002 Paris",
|
||
"livreur_assign": "john_deliveryman",
|
||
"dest_latitude": 48.8698,
|
||
"dest_longitude": 2.3311,
|
||
"created_at": "2025-01-18T14:40:00Z",
|
||
"updated_at": "2025-01-18T14:42:00Z"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Modifier le statut d'une commande
|
||
|
||
```bash
|
||
PUT /api/v2/admin/protected/orders/:id/status
|
||
Authorization: Bearer <admin_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{ "status": "arrived" }
|
||
```
|
||
|
||
**Statuts acceptés:** `pending`, `assigned`, `en_route`, `arrived`, `livre`, `approved`, `cancelled`
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Statut mis à jour",
|
||
"command_id": 1234,
|
||
"new_status": "arrived"
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Statut invalide
|
||
- `403` - Rôle insuffisant (admin ou cabine requis)
|
||
- `404` - Commande non trouvée
|
||
|
||
---
|
||
|
||
#### Notifier le client de descendre
|
||
|
||
Envoie une notification (Telegram + in-app) au client pour récupérer sa commande, puis passe le statut en `arrived`.
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/orders/:id/notify-client
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Client notifié",
|
||
"client_username": "jean_dupont"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Assignation automatique GPS
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/orders/:id/auto-assign
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Commande assignée automatiquement",
|
||
"command_id": 1234,
|
||
"assigned_to": "john_deliveryman",
|
||
"distance": {
|
||
"km": 1.5,
|
||
"eta_minutes": 8
|
||
},
|
||
"queue_position": 3
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `404` - Commande non trouvée
|
||
- `400` - Aucun livreur disponible
|
||
- `409` - Commande déjà assignée
|
||
|
||
---
|
||
|
||
#### Assigner toutes les commandes en attente
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/orders/auto-assign-all
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Auto-assignation effectuée",
|
||
"assigned_count": 5,
|
||
"failed_count": 0,
|
||
"details": [
|
||
{
|
||
"command_id": 1234,
|
||
"assigned_to": "john_deliveryman",
|
||
"distance_km": 1.5,
|
||
"eta_minutes": 8
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Gestion des Livreurs
|
||
|
||
#### Livreurs disponibles
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/delivery-persons
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"delivery_persons": [
|
||
{
|
||
"username": "john_deliveryman",
|
||
"status": "available",
|
||
"current_command": 0,
|
||
"queue_size": 2,
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"last_update": "2025-01-18T16:30:00Z"
|
||
}
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Position d'un livreur
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/delivery-persons/:username/location
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"deliveryman": "john_deliveryman",
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"last_update": "2025-01-18T16:25:00Z"
|
||
},
|
||
"status": "available"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Queues des livreurs
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/delivery/queues
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"queues": {
|
||
"john_deliveryman": {
|
||
"queue_size": 3,
|
||
"commands": [1234, 1235, 1236],
|
||
"can_accept_more": true,
|
||
"capacity": "3/10",
|
||
"status": "available"
|
||
}
|
||
},
|
||
"total_pending": 3
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Gestion des Produits
|
||
|
||
#### Créer un produit
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/products
|
||
Authorization: Bearer <admin_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"nom": "Pizza Pepperoni",
|
||
"category": "pizza",
|
||
"description": "Pizza avec pepperoni et mozzarella",
|
||
"stock": 30,
|
||
"prix": 14.50
|
||
}
|
||
```
|
||
|
||
**Réponse (201 Created):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Produit créé avec succès",
|
||
"product": {
|
||
"id": 10,
|
||
"nom": "Pizza Pepperoni",
|
||
"category": "pizza",
|
||
"description": "Pizza avec pepperoni et mozzarella",
|
||
"stock": 30,
|
||
"prix": 14.50,
|
||
"created_at": "2025-01-18T16:10:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Système de Pénalités (Clients uniquement)
|
||
|
||
Les amendes s'appliquent **uniquement aux clients**. Les livreurs n'ont pas d'amende.
|
||
|
||
Les amendes sont stockées dans `clients.amende` (PostgreSQL). Tant que `amende > 0`, le middleware `BlockClientIfPenalty` bloque toute tentative de checkout.
|
||
|
||
**Échelle progressive (annulations) :**
|
||
| Nbre d'annulations | Amende |
|
||
|---|---|
|
||
| 1ère | 20 € |
|
||
| 2ème | 50 € |
|
||
| 3ème | 100 € |
|
||
| 4ème et + | 150 € |
|
||
|
||
**Sources d'amende :**
|
||
- Client annule sa propre commande → `ApplyCancellationPenalty` (incrémente `cancellations_count`)
|
||
- Livreur marque le client absent depuis le statut `arrived` → amende appliquée automatiquement sur le **client**
|
||
|
||
**Message d'erreur au checkout bloqué :**
|
||
```json
|
||
{
|
||
"error": "Commande bloquée : vous avez une amende de 20€ en attente de paiement. Prenez attache avec Milieu Nantais sur signal pour régulariser votre situation..",
|
||
"amende": 20.0,
|
||
"blocked": true
|
||
}
|
||
```
|
||
|
||
#### Appliquer une pénalité (Admin)
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/penalty
|
||
Authorization: Bearer <admin_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"username": "jean_dupont",
|
||
"amount": 50.0,
|
||
"reason": "Comportement inapproprié"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Pénalité appliquée",
|
||
"client": "jean_dupont",
|
||
"penalty": {
|
||
"amount": 50.0,
|
||
"reason": "Comportement inapproprié",
|
||
"applied_at": "2025-01-18T17:00:00Z"
|
||
},
|
||
"total_amende": 70.0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🏪 API Cabine (v1)
|
||
|
||
**Toutes les routes cabine nécessitent le header:**
|
||
```
|
||
Authorization: Bearer <cabine_token>
|
||
```
|
||
|
||
#### Articles d'une commande
|
||
|
||
```bash
|
||
GET /api/v1/cabine/commands/:id/items
|
||
Authorization: Bearer <cabine_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"command_id": 1234,
|
||
"items": [
|
||
{
|
||
"id": 1,
|
||
"command_id": 1234,
|
||
"produit": "Pizza Margherita",
|
||
"quantite": 2,
|
||
"prix": 25.0,
|
||
"status": "pending",
|
||
"category": "pizza"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Modifier le statut d'une commande
|
||
|
||
```bash
|
||
PUT /api/v1/cabine/commands/:id/status
|
||
Authorization: Bearer <cabine_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{ "status": "arrived" }
|
||
```
|
||
|
||
**Statuts acceptés:** `pending`, `assigned`, `en_route`, `arrived`, `livre`, `approved`, `cancelled`
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Statut mis à jour",
|
||
"command_id": 1234,
|
||
"new_status": "arrived"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Notifier le client de descendre
|
||
|
||
```bash
|
||
POST /api/v1/cabine/commands/:id/notify-client
|
||
Authorization: Bearer <cabine_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Client notifié",
|
||
"client_username": "jean_dupont"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Modifier le statut d'un article
|
||
|
||
```bash
|
||
PUT /api/v1/cabine/items/:item_id/status
|
||
Authorization: Bearer <cabine_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"status": "prepared"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Statut mis à jour",
|
||
"item": {
|
||
"id": 1,
|
||
"command_id": 1234,
|
||
"status": "prepared",
|
||
"updated_at": "2025-01-18T17:15:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Statuts disponibles:** `pending`, `prepared`, `packed`, `ready`
|
||
|
||
---
|
||
|
||
#### Confirmer réception (approbation finale)
|
||
|
||
```bash
|
||
POST /api/v1/cabine/commands/:id/confirm-reception
|
||
Authorization: Bearer <cabine_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Réception confirmée",
|
||
"command_id": 1234,
|
||
"new_status": "approved"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Assigner un livreur
|
||
|
||
```bash
|
||
POST /api/v1/cabine/commands/:id/assign
|
||
Authorization: Bearer <cabine_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{ "username": "john_deliveryman" }
|
||
```
|
||
|
||
---
|
||
|
||
#### Livreurs disponibles
|
||
|
||
```bash
|
||
GET /api/v1/cabine/all/deliveryman
|
||
Authorization: Bearer <cabine_token>
|
||
```
|
||
|
||
---
|
||
|
||
## 🚚 API Livreur (v1)
|
||
|
||
**Toutes les routes livreur nécessitent le header:**
|
||
```
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
### Livraisons
|
||
|
||
#### Mes livraisons (données filtrées)
|
||
|
||
```bash
|
||
GET /api/v1/livreur/deliveries
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"deliveries": [
|
||
{
|
||
"id": 1234,
|
||
"status": "assigned",
|
||
"adresse": "15 Rue de la Paix, 75002 Paris",
|
||
"total_prix": 25.0,
|
||
"created_at": "2025-01-18T14:40:00Z",
|
||
"client_info": {
|
||
"nom": "Dupont",
|
||
"prenom": "Jean"
|
||
},
|
||
"items": [
|
||
{
|
||
"produit": "Pizza Margherita",
|
||
"quantite": 2,
|
||
"prix": 25.0
|
||
}
|
||
],
|
||
"items_count": 1,
|
||
"eta": {
|
||
"minutes": 25,
|
||
"estimated_arrival": "15:05"
|
||
}
|
||
}
|
||
],
|
||
"count": 1
|
||
}
|
||
```
|
||
|
||
**Note:** Les données sensibles comme le téléphone client sont filtrées pour les livreurs.
|
||
|
||
---
|
||
|
||
#### Mettre à jour le statut d'une livraison
|
||
|
||
```bash
|
||
PUT /api/v1/livreur/deliveries/:id/status
|
||
Authorization: Bearer <livreur_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"status": "en_route",
|
||
"notes": "En route vers le client",
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Statut mis à jour",
|
||
"command_id": 1234,
|
||
"status": "en_route",
|
||
"eta_minutes": 15,
|
||
"eta_message": "Arrivée prévue dans 15 minutes"
|
||
}
|
||
```
|
||
|
||
**Statuts valides pour livreur (via cet endpoint) :**
|
||
- `en_route` — En route vers le client (déclenche calcul ETA + notification Telegram avec ETA)
|
||
- `arrived` — Arrivé à destination (notifie le client de descendre)
|
||
- `livre` — Livré (via validation GPS uniquement, voir endpoint `force-validate`)
|
||
- `cancelled` — Annulation par le livreur
|
||
|
||
> **Note :** Le passage à `livre` via ce endpoint requiert des coordonnées GPS valides pour la validation de proximité.
|
||
|
||
> **Amende automatique :** si le livreur annule une commande dont le statut courant est `arrived`, une amende progressive est automatiquement appliquée au client (`ApplyCancellationPenalty`) — échelle : 1ère=20€, 2ème=50€, 3ème=100€, 4ème+=150€. Le livreur peut déclencher cette annulation via le bouton **"Client absent"** (cf. section ci-dessous).
|
||
|
||
---
|
||
|
||
### Bouton "Client absent" (DashboardScreen)
|
||
|
||
Lorsque le statut d'une livraison passe à `arrived`, un timer de **5 minutes** démarre automatiquement dans l'app livreur (`frontend-admin`). Pendant ce délai, un compte à rebours est affiché sur la carte de livraison. Une fois les 5 minutes écoulées, le bouton **"Client absent"** apparaît.
|
||
|
||
**Comportement :**
|
||
- Appui sur "Client absent" → annulation de la commande avec la raison `client_absent`
|
||
- L'API reçoit `status: cancelled` + `issue_type: client_absent`
|
||
- Le backend applique automatiquement `ApplyCancellationPenalty(clientUsername)` (amende progressive)
|
||
- Le timer est stocké dans un `useRef` (pas de re-render) et comparé à `Date.now()` toutes les secondes
|
||
|
||
**Implémentation :** `frontend-admin/src/screens/delivery/DashboardScreen.tsx` — constante `ABSENT_TIMEOUT_SECS = 300`, ref `arrivedAtRef`, état `elapsedSeconds`.
|
||
|
||
---
|
||
|
||
### Position GPS
|
||
|
||
#### Mettre à jour ma position
|
||
|
||
```bash
|
||
POST /api/v1/livreur/location/update
|
||
Authorization: Bearer <livreur_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Position mise à jour",
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"updated_at": "2025-01-18T17:35:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Ma position actuelle
|
||
|
||
```bash
|
||
GET /api/v1/livreur/location
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"last_update": "2025-01-18T17:35:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Statut et Queue
|
||
|
||
#### Mettre à jour mon statut
|
||
|
||
```bash
|
||
POST /api/v1/livreur/update/status
|
||
Authorization: Bearer <livreur_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requête:**
|
||
```json
|
||
{
|
||
"status": "available"
|
||
}
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Statut mis à jour",
|
||
"status": {
|
||
"username": "john_deliveryman",
|
||
"status": "available",
|
||
"updated_at": "2025-01-18T17:40:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Statuts disponibles:** `available`, `busy`, `offline`
|
||
|
||
---
|
||
|
||
#### Ma queue de livraisons
|
||
|
||
```bash
|
||
GET /api/v1/livreur/queue
|
||
Authorization: Bearer <livreur_token>
|
||
```
|
||
|
||
**Réponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"deliveryman": "john_deliveryman",
|
||
"queue_size": 3,
|
||
"commands": [
|
||
{
|
||
"position": 1,
|
||
"command_id": 1234,
|
||
"address": "15 Rue de la Paix, 75002 Paris",
|
||
"estimated_eta": 12,
|
||
"total_prix": 25.0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🗺️ Systeme GPS Integre
|
||
|
||
Le systeme GPS est au coeur de la plateforme de livraison. Il permet l'assignation automatique des livreurs, le calcul d'ETA en temps reel, et le suivi des livraisons.
|
||
|
||
### Architecture GPS
|
||
|
||
#### Flux GPS Complet
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[1. Commande creee par le Client] --> B[2. Geocodage adresse]
|
||
B --> |Nominatim API| C[(Cache Redis 7 jours)]
|
||
C --> D[3. Worker Auto-Assignation]
|
||
D --> |Toutes les 5 min| E[4. Recherche livreur le plus proche]
|
||
E --> |Haversine + TomTom| F[5. Calcul ETA avec trafic reel]
|
||
F --> G[6. Assignation a la queue du livreur]
|
||
G --> H[7. Livreur accepte et met a jour sa position]
|
||
H --> |Redis Pub/Sub| I[8. Client/Admin recoivent les mises a jour]
|
||
I --> J[9. Navigation vers destination]
|
||
J --> |TomTom Routing| K[10. Livraison terminee]
|
||
```
|
||
|
||
#### Architecture des Services GPS
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Frontend
|
||
MAP[TomTomMap Component]
|
||
UI[Interface Utilisateur]
|
||
end
|
||
|
||
subgraph Backend
|
||
GEO[Service Geocodage]
|
||
DIST[Service Distance]
|
||
ETA[Service ETA]
|
||
ASSIGN[Service Auto-Assignation]
|
||
end
|
||
|
||
subgraph APIs Externes
|
||
NOM[Nominatim API]
|
||
TOM[TomTom API]
|
||
end
|
||
|
||
subgraph Stockage
|
||
REDIS[(Redis Cache)]
|
||
PG[(PostgreSQL)]
|
||
end
|
||
|
||
UI --> MAP
|
||
MAP --> TOM
|
||
GEO --> NOM
|
||
GEO --> REDIS
|
||
DIST --> GEO
|
||
ETA --> TOM
|
||
ETA --> DIST
|
||
ASSIGN --> DIST
|
||
ASSIGN --> ETA
|
||
ASSIGN --> PG
|
||
ASSIGN --> REDIS
|
||
```
|
||
|
||
### Services GPS Utilises
|
||
|
||
| Service | Utilisation | Cache |
|
||
|---------|-------------|-------|
|
||
| **Nominatim** (OpenStreetMap) | Geocodage d'adresses | Redis 7 jours |
|
||
| **TomTom Routing API** | ETA avec trafic reel, itineraires | Non |
|
||
| **Haversine** (local) | Calcul de distance a vol d'oiseau | Non |
|
||
|
||
### Configuration Requise
|
||
|
||
**Variables d'environnement Backend:**
|
||
```bash
|
||
TOMTOM_API_KEY=<votre_cle_api_tomtom>
|
||
```
|
||
|
||
**Variables d'environnement Frontend:**
|
||
```bash
|
||
VITE_TOMTOM_API_KEY=<votre_cle_api_tomtom>
|
||
```
|
||
|
||
> **Note:** Obtenez une cle API TomTom gratuite sur [developer.tomtom.com](https://developer.tomtom.com/)
|
||
|
||
---
|
||
|
||
### Geocodage d'Adresses
|
||
|
||
Le geocodage convertit une adresse textuelle en coordonnees GPS (latitude/longitude).
|
||
|
||
#### Geocoder une adresse
|
||
|
||
```bash
|
||
POST /api/v1/geocode
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requete:**
|
||
```json
|
||
{
|
||
"address": "15 Rue de la Paix, 75002 Paris, France"
|
||
}
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"address": "15 Rue de la Paix, 75002 Paris, France",
|
||
"coordinates": {
|
||
"latitude": 48.8698,
|
||
"longitude": 2.3311
|
||
},
|
||
"cached": false
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Adresse manquante ou invalide
|
||
- `404` - Adresse non trouvee (geocodage echoue)
|
||
|
||
---
|
||
|
||
#### Valider une adresse
|
||
|
||
Verifie si une adresse peut etre geocodee sans la stocker.
|
||
|
||
```bash
|
||
POST /api/v1/validate-address
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requete:**
|
||
```json
|
||
{
|
||
"address": "15 Rue de la Paix, 75002 Paris"
|
||
}
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"valid": true,
|
||
"address": "15 Rue de la Paix, 75002 Paris",
|
||
"coordinates": {
|
||
"latitude": 48.8698,
|
||
"longitude": 2.3311
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Calcul de Distance et ETA
|
||
|
||
#### Formule Haversine
|
||
|
||
Le systeme utilise la formule Haversine pour calculer la distance a vol d'oiseau entre deux points GPS:
|
||
|
||
```
|
||
a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2)
|
||
c = 2 × atan2(√a, √(1-a))
|
||
distance = R × c
|
||
|
||
Ou R = 6371 km (rayon de la Terre)
|
||
```
|
||
|
||
#### ETA avec Trafic (TomTom)
|
||
|
||
L'ETA est calcule en utilisant l'API TomTom qui prend en compte:
|
||
- Les conditions de trafic en temps reel
|
||
- Les incidents routiers
|
||
- Les travaux
|
||
- L'heure de la journee
|
||
|
||
**Rotation automatique des clés :** jusqu'à 3 clés TomTom peuvent être configurées (`TOMTOM_API_KEY`, `TOMTOM_API_KEY_1`, `TOMTOM_API_KEY_2`, `TOMTOM_API_KEY_3`). En cas de quota dépassé (HTTP 403/429), le système passe automatiquement à la clé suivante sans interruption de service.
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[Demande ETA] --> B{Clés TomTom configurées?}
|
||
B -->|Non| E[Calcul Haversine]
|
||
B -->|Oui| C[Appel TomTom — clé active]
|
||
C --> Q{Quota dépassé 403/429?}
|
||
Q -->|Oui| R{Clé suivante disponible?}
|
||
R -->|Oui| C
|
||
R -->|Non| E
|
||
Q -->|Non| D[ETA avec trafic réel]
|
||
E --> F[Distance à vol d'oiseau]
|
||
F --> G[Vitesse moyenne 30 km/h]
|
||
G --> H[ETA estimé]
|
||
D --> I[Retourner ETA]
|
||
H --> I
|
||
```
|
||
|
||
**Fallback:** Si toutes les clés TomTom sont épuisées ou indisponibles, le système utilise un calcul local base sur:
|
||
- Distance Haversine
|
||
- Vitesse moyenne estimee (30 km/h en ville)
|
||
|
||
---
|
||
|
||
### Auto-Assignation GPS
|
||
|
||
Le systeme assigne automatiquement les commandes au livreur le plus proche.
|
||
|
||
#### Processus d'Auto-Assignation
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[Nouvelle Commande] --> B{Adresse geocodee?}
|
||
B -->|Non| C[Geocoder adresse]
|
||
C --> D
|
||
B -->|Oui| D[Recuperer livreurs disponibles]
|
||
D --> E[Calculer distances Haversine]
|
||
E --> F[Trier par proximite]
|
||
F --> G{Livreur avec capacite?}
|
||
G -->|Oui| H[Calculer ETA TomTom]
|
||
G -->|Non| I{Forcer assignation?}
|
||
I -->|Oui| H
|
||
I -->|Non| J[Commande en attente]
|
||
H --> K[Assigner a la queue]
|
||
K --> L[Notifier livreur]
|
||
```
|
||
|
||
#### Regles de Capacite
|
||
|
||
- Chaque livreur peut avoir jusqu'a **10 commandes** dans sa queue
|
||
- Si tous les livreurs sont a capacite maximale, le systeme peut forcer l'assignation
|
||
- Les livreurs avec le statut `offline` ne recoivent pas de commandes
|
||
|
||
#### Auto-assigner une commande specifique
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/orders/:id/auto-assign
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Commande assignee automatiquement",
|
||
"command_id": 1234,
|
||
"assigned_to": "john_deliveryman",
|
||
"assignment_details": {
|
||
"distance_km": 1.5,
|
||
"eta_minutes": 8,
|
||
"queue_position": 3,
|
||
"method": "nearest_available"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Lister les livreurs par distance
|
||
|
||
Obtient la liste des livreurs tries par distance depuis une adresse.
|
||
|
||
```bash
|
||
POST /api/v2/admin/protected/delivery/distances
|
||
Authorization: Bearer <admin_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requete:**
|
||
```json
|
||
{
|
||
"address": "15 Rue de la Paix, 75002 Paris"
|
||
}
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"address": "15 Rue de la Paix, 75002 Paris",
|
||
"destination": {
|
||
"latitude": 48.8698,
|
||
"longitude": 2.3311
|
||
},
|
||
"delivery_persons": [
|
||
{
|
||
"username": "john_deliveryman",
|
||
"distance_km": 1.5,
|
||
"eta_minutes": 8,
|
||
"status": "available",
|
||
"queue_size": 2,
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522
|
||
}
|
||
},
|
||
{
|
||
"username": "jane_delivery",
|
||
"distance_km": 3.2,
|
||
"eta_minutes": 15,
|
||
"status": "available",
|
||
"queue_size": 5,
|
||
"location": {
|
||
"latitude": 48.8456,
|
||
"longitude": 2.3789
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Liens de Navigation
|
||
|
||
Le systeme genere des liens vers plusieurs applications de cartographie.
|
||
|
||
#### Obtenir les liens carte d'un livreur
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/delivery-persons/:username/map-links
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"deliveryman": "john_deliveryman",
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522
|
||
},
|
||
"map_links": {
|
||
"google_maps": "https://www.google.com/maps?q=48.8566,2.3522",
|
||
"google_maps_app": "comgooglemaps://?center=48.8566,2.3522",
|
||
"waze": "https://www.waze.com/ul?ll=48.8566,2.3522&navigate=yes",
|
||
"waze_app": "waze://?ll=48.8566,2.3522&navigate=yes",
|
||
"apple_maps": "https://maps.apple.com/?ll=48.8566,2.3522",
|
||
"openstreetmap": "https://www.openstreetmap.org/?mlat=48.8566&mlon=2.3522",
|
||
"bing_maps": "https://www.bing.com/maps?cp=48.8566~2.3522",
|
||
"here_maps": "https://share.here.com/l/48.8566,2.3522"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Obtenir les liens de navigation pour une commande
|
||
|
||
Genere des liens de navigation depuis la position du livreur vers la destination de livraison.
|
||
|
||
```bash
|
||
GET /api/v2/admin/protected/commands/:id/navigation-links
|
||
Authorization: Bearer <admin_token>
|
||
```
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"command_id": 1234,
|
||
"origin": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"description": "Position du livreur"
|
||
},
|
||
"destination": {
|
||
"latitude": 48.8698,
|
||
"longitude": 2.3311,
|
||
"address": "15 Rue de la Paix, 75002 Paris"
|
||
},
|
||
"navigation_links": {
|
||
"google_maps": "https://www.google.com/maps/dir/48.8566,2.3522/48.8698,2.3311",
|
||
"waze": "https://www.waze.com/ul?ll=48.8698,2.3311&navigate=yes&from=48.8566,2.3522",
|
||
"apple_maps": "https://maps.apple.com/?saddr=48.8566,2.3522&daddr=48.8698,2.3311"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Gestion des Positions en Temps Reel
|
||
|
||
#### Flux de Mise a Jour de Position
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant L as Livreur (App)
|
||
participant API as Backend API
|
||
participant R as Redis
|
||
participant PS as Redis Pub/Sub
|
||
participant A as Admin/Client
|
||
|
||
L->>API: POST /location/update {lat, lng}
|
||
API->>API: Valider coordonnees
|
||
API->>R: SET delivery:location:{username}
|
||
API->>PS: PUBLISH position_update
|
||
PS-->>A: Notification temps reel
|
||
API-->>L: 200 OK {location, updated_at}
|
||
```
|
||
|
||
#### Mise a jour de position (Livreur)
|
||
|
||
```bash
|
||
POST /api/v1/livreur/location/update
|
||
Authorization: Bearer <livreur_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
**Requete:**
|
||
```json
|
||
{
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522
|
||
}
|
||
```
|
||
|
||
**Validation des coordonnees:**
|
||
- Latitude: entre -90 et +90
|
||
- Longitude: entre -180 et +180
|
||
|
||
**Reponse (200 OK):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Position mise a jour",
|
||
"location": {
|
||
"latitude": 48.8566,
|
||
"longitude": 2.3522,
|
||
"updated_at": "2025-01-18T17:35:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Erreurs possibles:**
|
||
- `400` - Coordonnees invalides (hors limites)
|
||
- `401` - Non authentifie
|
||
|
||
---
|
||
|
||
### Stockage Redis des Positions
|
||
|
||
Les positions GPS sont stockees dans Redis pour un acces rapide:
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph Redis Cache
|
||
A[delivery:location:username<br/>TTL: 2h]
|
||
B[delivery:status:username<br/>TTL: 1h]
|
||
C[geocode:cache:address_hash<br/>TTL: 7j]
|
||
D[command:destination:id<br/>TTL: 4h]
|
||
end
|
||
|
||
subgraph Donnees
|
||
A --> A1[latitude, longitude, updated_at]
|
||
B --> B1[status, latitude, longitude]
|
||
C --> C1[latitude, longitude, address]
|
||
D --> D1[dest_lat, dest_lng]
|
||
end
|
||
```
|
||
|
||
|
||
| `command:destination:{command_id}` | Coordonnees destination | 4 heures |
|
||
|
||
---
|
||
|
||
### Composant Carte Frontend (TomTomMap)
|
||
|
||
Le frontend inclut un composant React/TypeScript pour afficher la carte interactive.
|
||
|
||
#### Fonctionnalites du Composant
|
||
|
||
- **Carte interactive** avec marqueurs livreur/destination
|
||
- **Calcul d'itineraire** en temps reel
|
||
- **Instructions de navigation** (tourner a gauche, a droite, rond-point, etc.)
|
||
- **Affichage distance/duree** du trajet
|
||
- **Mode 3D** avec orientation selon la direction
|
||
- **Support francais** pour toutes les instructions
|
||
|
||
#### Exemple d'Utilisation
|
||
|
||
```tsx
|
||
import TomTomMap from './components/TomTomMap';
|
||
|
||
function DeliveryTracking() {
|
||
return (
|
||
<TomTomMap
|
||
driverPosition={{ lat: 48.8566, lng: 2.3522 }}
|
||
destinationPosition={{ lat: 48.8698, lng: 2.3311 }}
|
||
showRoute={true}
|
||
showInstructions={true}
|
||
/>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### Props du Composant
|
||
|
||
| Prop | Type | Description |
|
||
|------|------|-------------|
|
||
| `driverPosition` | `{lat: number, lng: number}` | Position du livreur |
|
||
| `destinationPosition` | `{lat: number, lng: number}` | Destination de livraison |
|
||
| `showRoute` | `boolean` | Afficher l'itineraire |
|
||
| `showInstructions` | `boolean` | Afficher le panneau d'instructions |
|
||
| `onEtaUpdate` | `(eta: number) => void` | Callback quand l'ETA change |
|
||
|
||
---
|
||
|
||
### Worker d'Auto-Assignation
|
||
|
||
Un worker CRON s'execute automatiquement pour assigner les commandes en attente.
|
||
|
||
#### Configuration
|
||
|
||
- **Frequence:** Toutes les 1 minute
|
||
- **Fichier:** `backend/gestion/workers/cron_auto_assign.go`
|
||
|
||
#### Processus du Worker
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
START((Demarrage CRON)) --> A[Recuperer commandes pending]
|
||
A --> B{Commandes a traiter?}
|
||
B -->|Non| END((Fin))
|
||
B -->|Oui| C[Prendre commande suivante]
|
||
C --> D{Adresse geocodee?}
|
||
D -->|Non| E[Geocoder via Nominatim]
|
||
E --> F
|
||
D -->|Oui| F[Recuperer livreurs disponibles]
|
||
F --> G[Calculer distances]
|
||
G --> H[Selectionner le plus proche]
|
||
H --> I{Capacite disponible?}
|
||
I -->|Non| J[Marquer pour retry]
|
||
I -->|Oui| K[Calculer ETA TomTom]
|
||
K --> L[Assigner commande]
|
||
L --> M[Mettre a jour statut]
|
||
M --> N{Autres commandes?}
|
||
J --> N
|
||
N -->|Oui| C
|
||
N -->|Non| O[Logger resultats]
|
||
O --> END
|
||
```
|
||
|
||
#### Logs d'Exemple
|
||
|
||
```
|
||
[AUTO-ASSIGN] Processing 5 pending orders
|
||
[AUTO-ASSIGN] Order #1234: Geocoded to (48.8698, 2.3311)
|
||
[AUTO-ASSIGN] Order #1234: Nearest driver is john_deliveryman (1.5 km)
|
||
[AUTO-ASSIGN] Order #1234: ETA calculated: 8 minutes
|
||
[AUTO-ASSIGN] Order #1234: Assigned successfully
|
||
[AUTO-ASSIGN] Completed: 5 assigned, 0 failed
|
||
```
|
||
|
||
---
|
||
|
||
### Cycle de Vie d'une Livraison (GPS)
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> pending: Commande creee (checkout)
|
||
pending --> assigned: Auto-assignation GPS / Admin
|
||
pending --> cancelled: Annulation client ou admin
|
||
|
||
assigned --> en_route: Livreur demarre (start)
|
||
assigned --> cancelled: Annulation
|
||
|
||
en_route --> arrived: Livreur arrive (GPS ou Admin/Cabine)
|
||
en_route --> en_route: Mise a jour position GPS
|
||
en_route --> livre: Validation GPS livreur / Admin
|
||
|
||
arrived --> livre: Remise au client (validation GPS livreur)
|
||
arrived --> approved: Admin/Cabine force approbation
|
||
|
||
livre --> approved: Client confirme / Admin / Cabine
|
||
|
||
approved --> [*]
|
||
cancelled --> [*]
|
||
|
||
note right of arrived
|
||
Declenche par :
|
||
- Livreur PUT status=arrived
|
||
- Admin/Cabine bouton "Le livreur est la"
|
||
Notifie le client (Telegram + push)
|
||
Timer 5min → bouton "Client absent"
|
||
Annulation depuis arrived → amende client
|
||
end note
|
||
|
||
note right of en_route
|
||
ETA calcule et stocke dans Redis
|
||
(command:eta:{id})
|
||
Utilise pour les notifications Telegram
|
||
end note
|
||
```
|
||
|
||
---
|
||
|
||
### Gestion des Erreurs GPS
|
||
|
||
#### Geocodage Echoue
|
||
|
||
Si une adresse ne peut pas etre geocodee:
|
||
- La commande reste en statut `pending`
|
||
- Un log d'erreur est genere
|
||
- L'admin peut corriger l'adresse manuellement
|
||
|
||
#### API TomTom Indisponible / Quota dépassé
|
||
|
||
Le système tente d'abord toutes les clés disponibles en rotation, puis bascule sur le calcul local :
|
||
- Utilise la formule Haversine pour la distance
|
||
- Estime l'ETA avec une vitesse moyenne de 30 km/h
|
||
- Un flag `fallback_used: true` est ajoute a la reponse
|
||
|
||
|
||
|
||
---
|
||
|
||
### Bonnes Pratiques
|
||
|
||
#### Pour les Livreurs
|
||
|
||
1. **Mettre a jour la position** frequemment (toutes les 30 secondes recommande)
|
||
2. **Verifier le statut** avant de commencer une livraison
|
||
3. **Utiliser les liens de navigation** generes par l'API
|
||
|
||
#### Pour les Admins
|
||
|
||
1. **Surveiller les queues** des livreurs pour eviter la surcharge
|
||
2. **Verifier les adresses** qui echouent au geocodage
|
||
3. **Utiliser l'endpoint distances** pour l'assignation manuelle si necessaire
|
||
|
||
#### Pour les Developpeurs
|
||
|
||
1. **Toujours valider** les coordonnees avant stockage
|
||
2. **Utiliser le cache** Redis pour les adresses frequentes
|
||
3. **Implementer un fallback** si TomTom est indisponible
|
||
4. **Logger les erreurs** de geocodage pour analyse
|
||
|
||
---
|
||
|
||
## ❌ Codes d'Erreur
|
||
|
||
### Erreurs HTTP Standards
|
||
|
||
| Code | Signification | Description |
|
||
|------|---------------|-------------|
|
||
| 200 | OK | Succès |
|
||
| 201 | Created | Ressource créée |
|
||
| 400 | Bad Request | Données invalides |
|
||
| 401 | Unauthorized | Non authentifié |
|
||
| 403 | Forbidden | Non autorisé |
|
||
| 404 | Not Found | Ressource introuvable |
|
||
| 409 | Conflict | Conflit (username déjà pris) |
|
||
| 422 | Unprocessable Entity | Validation échouée |
|
||
| 500 | Internal Server Error | Erreur serveur |
|
||
|
||
### Exemples d'Erreurs
|
||
|
||
#### 400 - Bad Request
|
||
```json
|
||
{
|
||
"error": "Données invalides",
|
||
"details": "Le champ 'username' est requis"
|
||
}
|
||
```
|
||
|
||
#### 401 - Unauthorized
|
||
```json
|
||
{
|
||
"error": "Token invalide ou expiré",
|
||
"message": "Veuillez vous reconnecter"
|
||
}
|
||
```
|
||
|
||
#### 403 - Forbidden
|
||
```json
|
||
{
|
||
"error": "Accès refusé",
|
||
"message": "Vous n'avez pas les permissions nécessaires"
|
||
}
|
||
```
|
||
|
||
#### 403 - Checkout bloqué (amende en attente)
|
||
```json
|
||
{
|
||
"error": "Commande bloquée : vous avez une amende de 20€ en attente de paiement. Prenez attache avec Milieu Nantais sur signal pour régulariser votre situation..",
|
||
"amende": 20.0,
|
||
"blocked": true
|
||
}
|
||
```
|
||
|
||
#### 404 - Not Found
|
||
```json
|
||
{
|
||
"error": "Ressource introuvable",
|
||
"message": "Commande avec l'ID 9999 introuvable"
|
||
}
|
||
```
|
||
|
||
#### 409 - Conflict
|
||
```json
|
||
{
|
||
"error": "Conflit",
|
||
"message": "Ce nom d'utilisateur est déjà pris"
|
||
}
|
||
```
|
||
|
||
#### 422 - Unprocessable Entity
|
||
```json
|
||
{
|
||
"error": "Validation échouée",
|
||
"details": {
|
||
"field": "telephone",
|
||
"message": "Format de téléphone invalide"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 500 - Internal Server Error
|
||
```json
|
||
{
|
||
"error": "Erreur serveur interne",
|
||
"message": "Une erreur inattendue s'est produite"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 Notes Importantes
|
||
|
||
### Sécurité
|
||
- **Tokens JWT** : Expiration variable selon le rôle (Client: 5h · Admin/Livreur/Cabine: 10h)
|
||
- **Validation GPS** : Requis pour certaines actions de livraison
|
||
- **Filtrage des données** : Les livreurs n'ont pas accès aux téléphones clients
|
||
- **Rate Limiting** : Protection contre les abus
|
||
|
||
### Format des Données
|
||
- **Dates** : Format ISO 8601 (`2025-01-18T14:30:00Z`)
|
||
- **Coordonnées** : Latitude/Longitude en décimal (WGS84)
|
||
- **Prix** : En euros avec 2 décimales
|
||
- **Téléphones** : Format international (`+33612345678`)
|
||
|
||
### Workers Automatiques
|
||
- **Auto-Assignment Cron** : Toutes les 1 minute (`workers/cron_auto_assign.go`)
|
||
- **Queue Cleanup** : Toutes les 5 minutes
|
||
- **ETA Updates** : Toutes les 30 secondes
|
||
- **Stock Cleanup** : Toutes les 5 minutes
|
||
|
||
### Fonctionnalités Avancées
|
||
- **Assignation GPS automatique** au checkout
|
||
- **Calcul ETA temps réel** avec TomTom API
|
||
- **Système de queues optimisé** pour les livreurs
|
||
- **Nettoyage automatique** des commandes invalides
|
||
- **Notifications temps réel** via Redis Pub/Sub
|
||
|
||
---
|
||
|
||
## 📋 Changelog
|
||
|
||
### v5.7.0 — 2026-08-04
|
||
|
||
- **Nouveau — choix du backend de stockage médias (`STORAGE_DRIVER`)** : les photos/vidéos produits pouvaient être stockées soit sur disque local (`CreateProduct`) soit sur S3/RustFS (`UploadMedia`) selon l'endpoint utilisé, sans logique commune. Une interface `Storage` unifiée (`services/storage.go`, implémentations `LocalStorage`/`S3Storage`) est maintenant utilisée par les deux endpoints, pilotée par la variable d'environnement `STORAGE_DRIVER` (`local` par défaut, ou `s3`).
|
||
- **Fix — nettoyage croisé des médias à la suppression** : `DeleteProduct` supprimait toujours sur disque (même pour un média stocké sur S3) et `DeleteMedia` supprimait toujours sur S3 (même pour un média local), laissant systématiquement des fichiers orphelins sur l'autre backend. Les deux fonctions branchent désormais sur `media.Key` (rempli uniquement pour les médias S3) pour cibler le bon backend, indépendamment du `STORAGE_DRIVER` courant.
|
||
- **Fix — correction d'adresse jamais appliquée au checkout (`CheckAddress`)** : le mécanisme de correction d'adresse (table `adresse_correction`, gérée par l'admin/cabine) détectait bien une adresse connue pour être mal formée, mais le checkout était systématiquement abandonné au lieu de proposer la correction de façon exploitable — le contrat de réponse (`corrected_address`) n'était lu nulle part côté client (mobile : parsing d'un format de message d'erreur obsolète ; web : aucune gestion de ce cas). Mobile et web lisent désormais directement `corrected_address` et proposent au client de reprendre le checkout avec l'adresse corrigée.
|
||
- **Fix — matching de correction d'adresse trop strict** : `CheckAddress` ne matchait qu'une égalité exacte de texte. Ajout d'un fallback normalisé (accents/casse/espaces ignorés, réutilise `utils.NormalizeAddress` déjà utilisée par le service de géocodage) pour rattraper les variantes mineures de saisie.
|
||
|
||
### v5.6.0 — 2026-07-11
|
||
|
||
- **Fix — ETA introuvable pour l'annulation tardive (`CheckCommandETAExistsAndValid`)** : la fonction lisait la clé Redis `command:eta:{id}` (un *hash*) avec `Redis.Get` (string), ce qui provoquait systématiquement une erreur `WRONGTYPE` silencieuse. Une ETA valide n'était donc jamais détectée par ce chemin, et un client pouvait annuler sans pénalité juste après l'assignation d'un livreur (avant le passage au statut `en_route`). Corrigé en `Redis.HGetAll`.
|
||
- **Fix — position livreur introuvable (`GetDeliverymanLocationForCommand`)** : même bug `WRONGTYPE` (`Redis.Get` sur un hash) empêchant l'affichage de la position temps réel sur certains suivis de commande. Corrigé en `Redis.HGetAll`.
|
||
- **Fix — ETA absente des notifications/app mobile/site web** : trois fonctions qui écrivent l'ETA dans Redis utilisaient des noms de champ incohérents (`eta_minutes` vs `total_eta_minutes`) alors que les clients ne lisent que `eta_minutes`. Les trois writers écrivent désormais les deux champs de façon cohérente.
|
||
- **Fix — boucle infinie de géocodage (`ResolveAddress` ↔ `GeocodeAddress`)** : la correction d'adresse mal écrite et le géocodage se rappelaient mutuellement sans condition de sortie pour toute adresse échouant au géocodage direct (le cas d'usage même de la correction), provoquant un blocage. `ResolveAddress` appelle désormais directement le cache/Nominatim sans repasser par `GeocodeAddress`.
|
||
- **Fix — récompenses par points non atomiques** : `ClaimPoolReward` (consommation des points) et `AddRewardsToBasket` (ajout du produit au panier) étaient deux étapes séparées ; un produit récompense supprimé/introuvable faisait perdre la récompense au client sans qu'il reçoive rien. Fusionnées dans `ClaimPoolRewardAndAddToBasket`, exécutée dans une seule transaction.
|
||
- **Fix — annulation admin non atomique** : `UpdateCommandStatusAdmin` pouvait rembourser deux fois le stock en cas d'appels concurrents. Bascule sur `CancelCommandByAdminAtomic` (transaction + verrou `FOR UPDATE`).
|
||
- **Fix — proratisation du chiffre d'affaires (stats)** : les commandes avec `referral_used` n'étaient pas correctement proratisées dans les statistiques de revenu.
|
||
- **Notifications — durée de rétention réduite à 1h** : les notifications stockées dans Redis passent d'un TTL de 7 jours à 1 heure (cohérent avec leur usage temps réel, évite l'accumulation inutile).
|
||
- **Suppression de code mort supplémentaire** : nettoyage dans `CreateCommand` et `DeleteCommandItem` (rendu atomique).
|
||
- **Tests unitaires** : ajout de suites complètes couvrant la gestion de stock (checkout/annulation/items), les statistiques, le géocodage et la correction d'adresses mal écrites (algorithme pur + intégration réseau réelle rate-limitée), les calculs de temps/ETA de commande, les récompenses par points (déduction de stock, atomicité), et l'annulation de commande avec pénalité de retard (barème, cumul concurrentiel, détection via statut ou ETA, flux "client absent").
|
||
|
||
### v5.5.0 — 2026-07-08
|
||
|
||
- **Fix — amende annulation livreur (`ApplyCancellationPenalty`)** : l'amende appliquée quand un livreur marque le client absent écrasait le montant existant au lieu de l'additionner, et n'était pas protégée par un verrou (`FOR UPDATE`). Elle est désormais cumulative et transactionnelle, cohérente avec le chemin d'annulation client (`CancelAtomic`).
|
||
- **Fix — mot de passe loggé en clair** : la modification d'un client par un admin (`PUT /admin/protected/clients/:id`) journalisait le corps de requête complet, y compris le nouveau mot de passe. Le log ne contient plus de donnée sensible.
|
||
- **Fix — IDOR consultation d'alerte police** : un livreur pouvait consulter le détail de l'alerte d'un autre livreur en devinant l'ID (`GET /api/v1/livreur/alert/:id`). L'accès est désormais restreint à ses propres alertes ; admin et cabine conservent l'accès complet.
|
||
- **Nettoyage — code mort** : suppression des handlers et fonctions utilitaires non routés/non appelés (ancien module `handlers/cabine.go`, `RegisterClient`, `GetCurrentClient`, `GetCurrentAdmin`, `GetMyCompletedOrders`, `GetRealtimeStats`, `StartPaymentChecker`, et helpers internes associés), identifiés via `staticcheck` et `deadcode`.
|
||
|
||
### v5.4.0 — 2026-05-18
|
||
|
||
- **Rotation automatique des clés TomTom** : jusqu'à 3 clés configurables (`TOMTOM_API_KEY_1/2/3`). En cas de quota dépassé (403/429), le système passe à la clé suivante automatiquement sans interruption. Fallback Haversine si toutes les clés sont épuisées.
|
||
- **Sécurité — création d'utilisateurs** : seul un `admin` peut créer des comptes `livreur` ou `cabine` via l'API. La création de compte `admin` est entièrement bloquée via l'application — uniquement possible en base de données directement.
|
||
- **Sécurité — cabine** : le rôle `cabine` n'a plus aucun droit de création d'utilisateurs ou de clients (retiré côté backend).
|
||
- **Fix frontend** : `createUserByAdmin` appelait `/admin/auth/register` (inexistant) → corrigé vers `/admin/protected/users`.
|
||
- **WAF ModSecurity logs** : les logs nginx et l'audit log ModSecurity sont désormais montés sur l'hôte (`/var/log/waf/nginx/` et `/var/log/waf/modsec/`) via volumes Docker stables. La variable `MODSEC_AUDIT_LOG` redirige l'audit log vers un fichier (au lieu de stdout) pour collecte Wazuh.
|
||
- **Notifications Telegram uniquement** : les push notifications Expo (iOS/Android) sont abandonnées. Clients et livreurs reçoivent désormais toutes leurs alertes via Telegram.
|
||
|
||
### v5.3.0 — 2026-05-15
|
||
|
||
- **Prix inactifs filtrés côté client** : le flag `active_price` sur `PRODUCT_PRICES` permet de désactiver un tarif sans le supprimer. Les endpoints publics/client et les frontends web+mobile masquent automatiquement les prix inactifs. Admin et cabine voient tous les prix.
|
||
- **Désactivation au lieu de suppression** : dans la modal d'édition produit (admin), retirer un prix existant le désactive (`active_price = false`) plutôt que de le supprimer de la base.
|
||
- **Amende automatique client — livreur annule depuis `arrived`** : quand le livreur marque le client absent (`arrived` → `cancelled`), `ApplyCancellationPenalty` est appelé automatiquement sur le **client** (amende progressive : 20→50→100→150€).
|
||
- **Bouton "Client absent"** : après 5 minutes au statut `arrived`, l'app livreur affiche un bouton "Client absent" qui déclenche l'annulation avec amende sur le client.
|
||
- **Message d'erreur checkout avec contact** : le message de blocage inclut désormais "Prenez attache avec Milieu Nantais sur signal pour régulariser votre situation."
|
||
- **OTA channels par rôle** : les canaux Expo OTA sont désormais nommés `pre-prod-client` / `production-client` / `pre-prod-admin` / `production-admin` pour éviter les mises à jour croisées entre builds.
|
||
|
||
### v5.2.0 — 2026-05-01
|
||
|
||
- Activation/désactivation des prix par quantité (admin)
|
||
- Gestion de stock améliorée
|
||
- Refactoring handlers produits
|
||
|
||
---
|
||
|
||
**Documentation mise à jour le :** 2026-07-11
|
||
**Version API :** 5.6.0
|
||
**Technologies :** Go 1.24, Gin, PostgreSQL 16, Redis 7, React 19, Expo 54, TomTom API, ModSecurity WAF
|
||
**Déploiement :** Docker Compose · Nginx + ModSecurity OWASP CRS · TLS 1.2/1.3
|
||
**Base URL prod :** `https://mln-uber.club`
|
||
**Infrastructure :** WireGuard VPN · Wazuh SIEM · Dozzle · Beszel · ClamAV · MinIO S3
|