diff --git a/README.md b/README.md
index c69a2329..cab44093 100644
--- a/README.md
+++ b/README.md
@@ -680,17 +680,20 @@ Tous les serveurs backend communiquent via un réseau WireGuard privé `10.0.0.0
### Architecture DMZ / LAN
-```
-Internet
- │
- ▼
-prod-uber (DMZ — 185.103.166.119)
- │ 80/443 public
- │ ──── VPN (10.0.0.6) ────► bdd-redis-prod (LAN — 10.0.0.5)
- │ ├── PostgreSQL :5432
- │ └── Redis :6379
- │
-vpn-uber (10.0.0.1) — jump host SSH pour accès aux autres serveurs
+```mermaid
+graph TB
+ Internet((Internet)) -->|"80/443 public"| Prod["prod-uber — DMZ
185.103.166.119"]
+ Prod -->|"VPN (10.0.0.6)"| BDD["bdd-redis-prod — LAN
10.0.0.5"]
+ BDD --> PG["PostgreSQL :5432"]
+ BDD --> Redis["Redis :6379"]
+
+ VPN["vpn-uber — 10.0.0.1
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 :
@@ -826,6 +829,26 @@ 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 /
UploadMedia"] --> Check{"STORAGE_DRIVER"}
+ Check -->|"local (défaut)"| Local["LocalStorage
disque ./uploads"]
+ Check -->|"s3"| S3["S3Storage
RustFS (auto-hébergé)"]
+
+ Delete["DeleteProduct /
DeleteMedia"] --> KeyCheck{"media.Key
rempli ?"}
+ KeyCheck -->|non| Local
+ KeyCheck -->|oui| S3
```
---
@@ -1356,6 +1379,38 @@ Content-Type: application/json
- `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é
(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
@@ -3106,6 +3161,13 @@ Le système tente d'abord toutes les clés disponibles en rotation, puis bascule
## 📋 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`.