docs/40-relation/99-technique.md —
ce document se lit aussi dans le dépôt, sans serveur.
Clients et fournisseurs — le côté technique
Les fichiers
| Rôle | Fichier |
|---|---|
| Clients | public/erp/clients.php, public/erp/client.php |
| API clients | src/Http/Controllers/CustomerController.php, ClientController.php |
| Données clients | src/Woo/WooCustomerRepository.php |
| Fournisseurs | public/erp/fournisseurs.php, public/erp/fournisseur.php |
| API fournisseurs | src/Http/Controllers/SupplierController.php |
| Données fournisseurs | src/Repository/SupplierRepository.php |
| SAV | public/erp/sav.php + public/erp/assets/js/pages/sav-2.js |
| API SAV | src/Http/Controllers/SavController.php |
| API publique SAV | src/Http/Controllers/SupportController.php |
| L'action SAV | src/Orders/OrderActionService.php |
| Données SAV | src/Repository/CustomerContactRepository.php |
| Pièces jointes | src/Support/PiecesJointesSav.php |
| Avis | public/erp/avis.php, src/Http/Controllers/AvisController.php |
| Le délai de livraison | src/Avis/DelaiLivraison.php |
| Suivi des colis | src/Tracking/TrackingService.php |
| Le verrou de tâche | src/Support/RunLock.php |
| Limitation | src/Auth/LoginThrottle.php |
Les routes
| Route | Qui |
|---|---|
GET /v1/customers, /v1/clients/profile | Module orders |
GET /v1/suppliers, /{id}, /{id}/dashboard | Module suppliers, lecture |
POST/PUT /v1/suppliers | Module suppliers, écriture |
DELETE /v1/suppliers/{id} | Administrateur |
GET /v1/sav, /v1/sav/stats | Module support, lecture |
PUT /v1/sav/{id}/close | Module support, écriture |
POST /v1/orders/{id}/customer-contacts | Module orders, écriture |
GET /v1/avis, POST /v1/avis/* | Module orders |
POST /v1/support/verify, /submit | Public, limité en fréquence |
La table du SAV
customer_contacts : woo_order_id, contact_type, reply_to_id, customer_email, message, status_applied, email_status, created_by_user_id, created_by_email, created_at.
Un piège dans le schéma
status_applied porte deux sens. Soit le statut WooCommerce appliqué en même temps que le message, soit la valeur 'closed' qui dit « traité ».
Ça tient parce que les statuts WooCommerce sont tous préfixés wc- : closed n'entre jamais en collision. Mais c'est une colonne pour deux idées, et il faut le savoir avant d'y toucher.
IFNULL est obligatoire dans les conditions : une demande jamais touchée a NULL, et un simple <> 'closed' vaudrait NULL — la ligne disparaîtrait.
Le critère « entrant »
cc.created_by_user_id IS NULL. Toute action faite depuis l'ERP marque l'opérateur ; le formulaire public et l'import historique ne marquent personne.
Cela remplace l'ancienne règle contact_type <> 'sav_reply' : une réponse est toujours écrite par un opérateur.
Le formulaire public est un oracle
Les numéros de commande se suivent — environ 85 000.
La seule limite était un quota par IP, posé sur un en-tête que l'appelant choisit. Le coût d'un essai était donc d'une requête HTTP. Avec une liste d'adresses — celles d'une fuite, d'une base de prospection — on associait chaque adresse à ses commandes. Et la réponse donnait en prime le statut réel, que la page n'affiche même pas. Le couple connu, /submit permettait d'ouvrir un SAV au nom du client.
Trois changements :
- Le statut ne sort plus.
- Les échecs sont comptés par adresse et par numéro de commande, pas seulement par IP : l'un contre le balayage des commandes d'un client connu, l'autre contre le balayage des adresses sur une commande donnée.
- Le piège à robots est vérifié côté serveur. Un robot pris reçoit la même réponse qu'un couple inconnu : rien ne lui apprend qu'il a été vu.
C'est le même raisonnement que Certi'Clé.
Les pièces jointes, et la faute du 20/09/2026
Le formulaire WordPress dépose les fichiers sur le domaine de la boutique et envoie à Darsy le message du client suivi d'un marqueur :
[Fichiers joints: facture.pdf|sav_6ab0e92b6c68d4.05973337.pdf, …]
La fiche commande découpe là-dessus et fabrique un lien et une vignette.
Ce qui s'est passé
Un audit a conclu que « le formulaire public actuel n'envoie aucun fichier » — en lisant public/support/ de ce dépôt, qui n'est pas le formulaire des clients. Sur cette base, le marqueur a été neutralisé à l'entrée. La fiche commande, qui découpe sur l'orthographe exacte, s'est mise à afficher le marqueur en texte brut.
Le lendemain, un client a joint trois pièces. L'opérateur a vu du texte.
La leçon : ne pas juger le système vivant d'après ce qu'il y a dans le dépôt.
Ce qui protège vraiment
Le danger n'a jamais été le marqueur : c'est le nom stocké qu'il porte, parce qu'il est concaténé à une URL. Un client qui écrit facture.jpg|../../wp-login.php fait pointer une vignette d'apparence légitime ailleurs sur le domaine.
La règle est donc sur le nom : un nom de fichier, et rien d'autre — pas de barre, pas de remontée, pas d'espace.
Un nom refusé ne fait pas disparaître la pièce. Elle reste annoncée, en texte inerte, sans lien. L'effacer cacherait à l'opérateur qu'une pièce a été promise.
Le verrou de tâche
La vérification de suivi s'exécute dans la requête et peut durer une minute.
Le verrou s'appuie sur flock(), donc le système le libère seul si le processus meurt : pas de verrou fantôme après un plantage, contrairement à un simple fichier témoin.
Et il échoue ouvert. Si le stockage est indisponible, la tâche s'exécute quand même. Ce verrou évite un gaspillage, il ne protège pas une donnée : refuser de travailler parce qu'un répertoire temporaire est cassé coûterait plus cher que le doublon qu'il empêche.
Le piège des recherches
Un marqueur nommé utilisé deux fois dans une requête : PDO est en préparation native (ATTR_EMULATE_PREPARES = false), qui refuse avec HY093. Toute recherche sur la page SAV renvoyait 500.
Trois marqueurs distincts liés à la même valeur, et c'est réglé.
La saisie n'est pas un motif : un _ dans une adresse ou un % dans un message doit se chercher lui-même. Database\LikePattern les neutralise, et la clause ESCAPE dit au moteur comment les relire.
Les tests
tests/Unit/SurfacesPubliquesTest.php (la règle sur les noms de fichier, cas par cas, y compris la remontée de répertoire), tests/Unit/DelaiLivraisonTest.php (les fériés, Pâques, les bornes), tests/Unit/RunLockTest.php, tests/Unit/LikePatternTest.php, tests/Unit/LoginThrottleTest.php, tests/Unit/ConventionDeNommageTest.php, tests/Unit/VariationsTarifairesDuStockTest.php.