Aller au contenu

API — Commandes et checkout

Cette page documente l'ensemble du parcours checkout PerfShop, qui se déroule en 5 étapes réparties sur deux contrôleurs.

Contrôleurs couverts

  • CheckoutController (/api/checkout/*) → étapes 2 à 4 (adresse, livraison, paiement) + codes promo
  • OrderController (/api/orders/*) → étape 5 (validation finale) + consultation, annulation, facture

L'étape 1 (login) est documentée dans auth.md.


Les 5 étapes du parcours checkout

sequenceDiagram
    autonumber
    participant F as Frontend React
    participant B as Backend
    participant DB as MySQL

    Note over F: Panier rempli
    F->>B: POST /api/auth/login
    B-->>F: 200 { securityToken, tokens scripting }

    F->>B: POST /api/checkout/address
    Note right of B: Validation stricte<br/>ValidationService
    B-->>F: 200 { step: "address", nextStep: "shipping" }

    F->>B: POST /api/checkout/shipping
    Note right of B: standard / express / premium
    B-->>F: 200 { step: "shipping", deliveryDays, nextStep: "payment" }

    F->>B: POST /api/checkout/payment
    Note right of B: Luhn + CVV jeté immédiatement<br/>CVV jamais logué
    B-->>F: 200 { step: "payment", summary, nextStep: "confirm" }

    F->>B: POST /api/orders { securityToken, items }
    B->>DB: INSERT INTO orders, order_items, UPDATE stock
    B-->>F: 201 { success, orderNumber, orderId, totalAmount }

Chaque étape verrouille l'étape suivante : une tentative de POST /api/checkout/payment sans avoir d'abord soumis l'adresse retourne un 400 Bad Request. Les états intermédiaires sont stockés en HttpSession sous les clés checkout_address, checkout_shipping, etc.


Contrôle Chaos Scripting

Le parcours checkout est le seul chemin protégé par Chaos Scripting. Chaque étape valide des headers anti-rejeu dont la liste et la complexité dépendent du niveau actif :

Niveau Nom Headers requis à chaque étape
0 Désactivé Aucun
1 Junior X-Session-Token, X-Request-ID
2 Confirmé + X-Action-Token (renouvelé à chaque étape)
3 Expert + X-CSRF-Token, X-Step-Token, X-Signature (HMAC)
4 Maestro Idem + clé HMAC dérivée du sessionToken

Le détail des algorithmes est dans Chaos Scripting. Dans les exemples ci-dessous, on reste en niveau 0 (pas de headers).


Contrôleur CheckoutController


POST /api/checkout/address

Soumet l'adresse de livraison. C'est l'étape 2 du parcours (après le login).

Auth : session utilisateur + securityToken valide Contrôleur : CheckoutController.submitAddress() Validation : ValidationService.validateProfile()

Requête

{
  "street": "12 rue de la Paix",
  "postalCode": "75001",
  "city": "Paris",
  "region": "Île-de-France",
  "country": "FR",
  "phone": "0145678901"
}
Champ Type Requis Contrainte
street string oui 5–200 caractères
postalCode string oui Format national (FR : 5 chiffres)
city string oui 2–100 caractères, sans accents refusés
region string non 2–100 caractères
country string oui Code ISO 3166-1 alpha-2
phone string oui Format national, mobiles FR 06/07 rejetés pour livraison

Réponse — 200 OK

{
  "success": true,
  "step": "address",
  "nextStep": "shipping"
}

En Chaos Scripting niveau ≥ 1, la réponse inclut aussi des headers de rotation (X-Session-Token, etc.) à réutiliser pour l'étape suivante.

Codes d'erreur

Code Cause
400 Champ obligatoire manquant (clés checkout.error.*_required)
401 Non connecté (clé auth.error.not_authenticated)
422 Validation métier échouée (format téléphone, code postal, pays). Corps : {"error": "Données invalides", "fields": {...}}

Effets de bord en session

L'adresse complète est sérialisée et stockée dans HttpSession sous :

  • checkout_address (concaténation formatée)
  • checkout_city, checkout_country, checkout_phone
  • checkout_street, checkout_postalCode, checkout_region

Ces valeurs sont relues par OrderController.createOrder() à l'étape finale.


POST /api/checkout/shipping

Sélectionne le mode de livraison. Étape 3.

Auth : session + securityToken Prérequis : checkout_address présent en session

Requête

{ "shippingMethod": "express" }
Valeur Description
standard Livraison standard (3-5 jours)
express Livraison rapide (1-2 jours)
premium Livraison le lendemain avant 13h

Réponse — 200 OK

{
  "success": true,
  "step": "shipping",
  "nextStep": "payment",
  "shippingMethod": "express",
  "deliveryDays": 2,
  "deliveryLabel": "2 jour(s) ouvré(s)",
  "workingDays": true
}

A7 — Jours calendaires au lieu de jours ouvrés

En Chaos Métier niveau 2 et au-delà, le calcul du délai de livraison passe en jours calendaires au lieu de jours ouvrés. Les week-ends et jours fériés ne sont plus exclus. Une livraison « standard 3 jours ouvrés » peut tomber un dimanche. Les champs deliveryDays, deliveryLabel et workingDays reflètent le calcul corrompu. Voir A7.

Codes d'erreur

Code Cause
400 Mode de livraison invalide (clé checkout.error.shipping_invalid)
400 Adresse non soumise (clé checkout.error.address_first)
401 Non connecté

POST /api/checkout/payment

Soumet les informations de paiement. Étape 4.

Auth : session + securityToken Prérequis : checkout_address et checkout_shipping présents en session Validation : ValidationService.validateCard() (Luhn, expiration) + validateCvv()

Requête

{
  "cardHolder": "Alice Durand",
  "cardNumber": "4532015112830366",
  "expiryMonth": "12",
  "expiryYear": "2027",
  "cvv": "123"
}
Champ Validation
cardHolder Non vide
cardNumber 16 chiffres + algorithme de Luhn
expiryMonth 1–12
expiryYear Future, ≤ 2029
cvv 3 chiffres (ou 4 pour Amex)

Sécurité CVV

Le CVV est validé localement par ValidationService.validateCvv() puis immédiatement jeté. Il n'est :

  • jamais stocké en session
  • jamais logué, ni en clair ni haché
  • jamais transmis à OrderService

Seul un flag booléen cvvVerified est propagé à l'étape suivante, accompagné des 4 derniers chiffres de la carte (cardLast4) pour l'affichage de confirmation. C'est le comportement attendu en production — PerfShop l'implémente correctement malgré sa nature pédagogique.

Réponse — 200 OK

{
  "success": true,
  "step": "payment",
  "nextStep": "confirm",
  "summary": {
    "address": "12 rue de la Paix, 75001 Paris, Île-de-France, FR",
    "city": "Paris",
    "country": "FR",
    "shippingMethod": "express",
    "paymentMethod": "CARD",
    "cardHolder": "Alice Durand",
    "cardLast4": "0366"
  }
}

Codes d'erreur

Code Cause
400 Étapes précédentes manquantes (adresse ou livraison)
401 Non connecté
422 Carte invalide (Luhn, expiration, format) ou CVV invalide

Le corps des 422 suit la structure {"error": "...", "field": "..."}, par exemple {"error": "Numéro de carte invalide (échec vérification Luhn)", "field": "cardNumber"}.


POST /api/checkout/promo

Valide un code promotionnel et retourne le pourcentage de réduction applicable.

Auth : session + securityToken

Requête

{ "code": "PROMO10" }

Réponse — 200 OK (code valide)

{
  "valid": true,
  "code": "PROMO10",
  "discountPercent": 10,
  "message": "Code promo valide: PROMO10 -> 10% de reduction"
}

Réponse — 200 OK (code invalide, comportement nominal)

{
  "valid": false,
  "code": "INEXISTANT",
  "discountPercent": 0,
  "message": "Code promo invalide: INEXISTANT"
}

A6 — Code invalide silencieusement accepté

En Chaos Métier niveau 2 et au-delà, un code promo invalide est accepté avec valid: true et discountPercent: 0. L'utilisateur voit « Code promo appliqué » mais n'obtient aucune réduction. Aucun message d'erreur n'est retourné — l'anomalie est uniquement visible en comparant le total avant/après. Voir A6.

Codes d'erreur

Code Cause
400 Code manquant (checkout.error.promo_required)
401 Non connecté

GET /api/checkout/session

Retourne l'état courant de la session checkout — utile pour réhydrater un panier abandonné en milieu de parcours.

Auth : session + securityToken

Réponse — 200 OK

{
  "address": "12 rue de la Paix, 75001 Paris, Île-de-France, FR",
  "city": "Paris",
  "country": "FR",
  "shippingMethod": "express",
  "paymentMethod": "CARD",
  "cardHolder": "Alice Durand",
  "cardLast4": "0366"
}

Les champs non encore remplis valent null.


Contrôleur OrderController


GET /api/orders/checkout/verify

Vérifie que l'utilisateur courant peut accéder au parcours checkout. Utilisé par le frontend pour décider s'il faut rediriger vers le login.

Auth : session (optionnelle — retourne le statut dans tous les cas)

Réponse — 200 OK

{
  "canCheckout": true,
  "authenticated": true,
  "hasToken": true,
  "message": "Accès autorisé"
}
Champ Description
canCheckout true si authenticated et hasToken
authenticated Session utilisateur active
hasToken securityToken checkout encore valide

POST /api/orders

Étape 5 — validation finale de la commande. Crée la commande en base, décrémente les stocks, envoie la confirmation, et (si applicable) injecte le code agent pédagogique.

Auth : session utilisateur + securityToken valide dans le body Contrôleur : OrderController.createOrder() Service : OrderService.createOrderFromItems()

Requête

{
  "securityToken": "a7f1c2e8-4b9d-4f11-9c8e-1a2b3c4d5e6f",
  "items": [
    { "productId": 42, "quantity": 2 }
  ],
  "shippingAddress": "12 rue de la Paix, 75001 Paris",
  "shippingMethod": "express",
  "paymentMethod": "CARD"
}
Champ Type Requis Note
securityToken string (UUID) oui Généré par POST /api/auth/login
items[] array oui Au moins un article
items[].productId Long oui
items[].quantity Integer oui ≥ 1
items[].unitPrice Decimal non Ignoré en comportement nominal — voir S5
shippingAddress string non Relu depuis la session si absent
shippingMethod string non Relu depuis la session si absent
paymentMethod string non CARD par défaut

Si les champs d'adresse/livraison/paiement sont absents du body, le contrôleur les relit depuis la session (définis aux étapes précédentes). Cela permet un flux strict étape-par-étape OU un flux « one-shot » où le frontend envoie tout d'un coup.

Headers de requête

Header Usage
X-Student-Token Optionnel — si présent, le code agent pédagogique associé est injecté dans la réponse
Headers Chaos Scripting Selon le niveau actif

Réponse — 201 Created

{
  "success": true,
  "orderNumber": "PS-2026-000123",
  "orderId": 789,
  "totalAmount": 299.98
}

Avec un X-Student-Token valide associé à une session pédagogique, la réponse inclut aussi agentCode :

{
  "success": true,
  "orderNumber": "PS-2026-000123",
  "orderId": 789,
  "totalAmount": 299.98,
  "agentCode": "NEPTUNE-1284"
}

Le agentCode est calculé par PedagogiqueSessionService.getAgentCodeForToken() selon le niveau BAC actif. Voir Code agent dynamique.

Codes d'erreur

Code Cause
400 Panier vide (clé order.error.cart_empty)
401 Non authentifié — retourne aussi redirectTo: /login
403 Token invalide — retourne aussi redirectTo: /login
500 Erreur serveur ou chaos fonctionnel actif

S4 — XSS stocké via shippingAddress

En Chaos Sécurité niveau 2 et au-delà, le champ shippingAddress est stocké sans échappement HTML. Un payload <script>alert(1)</script> est persisté tel quel en base et ré-exécuté dans l'interface admin qui affiche les détails de commande. Voir S4.

S5 — Falsification du prix client

En Chaos Sécurité niveau 2 et au-delà, le champ unitPrice du body est accepté : si l'utilisateur envoie {"productId": 42, "quantity": 1, "unitPrice": 0.01}, la commande est enregistrée à 1 centime. L'écart entre le prix client et le prix réel en base est loggé mais n'empêche pas la commande. Voir S5.


GET /api/orders

Liste les commandes de l'utilisateur connecté, triées de la plus récente à la plus ancienne.

Auth : session Contrôleur : OrderController.getUserOrders()

Réponse — 200 OK

{
  "orders": [
    {
      "id": 789,
      "orderNumber": "PS-2026-000123",
      "totalAmount": 299.98,
      "status": "CONFIRMED",
      "createdAt": "2026-04-08T10:30:00Z",
      "itemCount": 1
    }
  ],
  "historyTotal": 1247.50
}
Champ Description
orders[] Liste des commandes
historyTotal Somme cumulée de toutes les commandes

Chaos actifs

Deadlock DB possible

L'endpoint invoque orderService.applyDeadlockChaosPublic() avant la lecture — en Chaos Performance actif, un deadlock peut faire remonter un 500 Internal Server Error avec le message Deadlock base de données détecté — transaction annulée.

A15 — Corruption silencieuse de l'historique

En Chaos Métier niveau 4, les totalAmount sont multipliés par 1.1 à l'affichage sans toucher aux valeurs en base. Le champ historyTotal suit la même corruption. Une commande à 100 € apparaît à 110 € dans la liste mais GET /api/orders/{id} retourne le bon montant si chargé depuis la DB. Voir A15.

A10 — Total historique faux

En Chaos Métier niveau 3, historyTotal est calculé avec un arrondi cumulé par ligne — le résultat diffère de quelques centimes de la somme réelle.


GET /api/orders/{id}

Retourne les infos d'une commande.

Auth : session — l'utilisateur ne peut voir que ses propres commandes

Réponse — 200 OK

L'entité Order sérialisée avec les relations (user, items).

Codes d'erreur

Code Cause
401 Non authentifié
403 Commande appartenant à un autre utilisateur (clé order.error.access_denied)
404 Commande introuvable

S2 — IDOR Commandes

En Chaos Sécurité niveau 1 et au-delà, la vérification d'appartenance est désactivée. Un utilisateur A peut lire les commandes de l'utilisateur B en devinant ou énumérant les IDs. L'accès cross-user est loggé (recordIdorAccess) mais n'est pas bloqué — la commande est retournée normalement. Voir S2.


GET /api/orders/{id}/details

Version enrichie de GET /api/orders/{id} — retourne les lignes de commande avec les noms des produits, quantités, prix et sous-totaux, dans une structure aplatie adaptée à l'affichage.

Auth : session (même règle IDOR que l'endpoint ci-dessus)

Réponse — 200 OK

{
  "id": 789,
  "orderNumber": "PS-2026-000123",
  "totalAmount": 299.98,
  "status": "CONFIRMED",
  "createdAt": "2026-04-08T10:30:00Z",
  "shippingAddress": "12 rue de la Paix, 75001 Paris",
  "shippingMethod": "express",
  "paymentMethod": "CARD",
  "items": [
    {
      "productName": "Casque audio Bluetooth",
      "quantity": 2,
      "unitPrice": 149.99,
      "subtotal": 299.98
    }
  ]
}

Codes d'erreur

Identiques à GET /api/orders/{id}. S2 (IDOR) s'applique également.


GET /api/orders/{id}/invoice

Endpoint de génération de facture simulée. Utilisé en pédagogie pour illustrer S8 (Path Traversal).

Auth : session

Paramètres

Paramètre Type Défaut Description
format (query) string pdf Format de la facture : pdf ou csv

Comportement nominal (niveau Sécurité 0-2)

Valide strictement format ∈ {"pdf", "csv"}. Retourne un contenu simulé :

{
  "orderId": 789,
  "format": "pdf",
  "content": "FACTURE #789\nPerfShop SAS — 123 rue du Commerce — 75001 Paris\nMerci pour votre commande."
}

Codes d'erreur (nominal)

Code Cause
400 Format invalide — seuls pdf et csv sont autorisés (clé order.error.format_invalid)
401 Non authentifié

S8 — Path Traversal

En Chaos Sécurité niveau 3 et au-delà, le paramètre format n'est plus validé. Un étudiant peut passer format=../../../etc/passwd et la simulation retourne un contenu marqué « PATH TRAVERSAL SIMULE » :

{
  "orderId": 789,
  "format": "/etc/passwd",
  "content": "root:x:0:0:root:/root:/bin/bash\n...",
  "warning": "PATH TRAVERSAL SIMULE — contenu non reel (pedagogique)"
}

Aucun fichier réel du système hôte n'est lu — le contenu est une simulation statique, mais le chemin de code démontre la vulnérabilité. Voir S8.


POST /api/orders/{id}/cancel

Annule une commande. Autorisée uniquement sur les statuts PENDING ou CONFIRMED.

Auth : session — vérification stricte d'appartenance

Réponse — 200 OK

{
  "success": true,
  "message": "Commande annulée avec succès",
  "orderNumber": "PS-2026-000123",
  "status": "CANCELLED"
}

Effets de bord — restocking

En fonctionnement nominal, l'annulation recrédit le stock de chaque article vers le produit parent (UPDATE products.stock += items[].quantity).

Codes d'erreur

Code Cause
400 Statut non annulable — {"error": "Impossible d'annuler une commande en statut SHIPPED", "status": "SHIPPED"}
401 Non authentifié
403 Commande d'un autre utilisateur
404 Commande introuvable

A16 — Annulation sans restocking

En Chaos Métier niveau 4, le restocking est supprimé : la commande passe bien en CANCELLED mais le stock n'est pas remis à niveau. Les articles sont « perdus » silencieusement. Aucune erreur retournée. Voir A16.


Exemple curl — parcours complet

# 1. Login
TOKEN=$(curl -s -c /tmp/c.txt \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"alice123"}' \
  http://localhost:9080/api/auth/login | jq -r '.securityToken')

# 2. Ajouter au panier
curl -b /tmp/c.txt -H "Content-Type: application/json" \
  -d '{"productId":42,"quantity":2}' \
  http://localhost:9080/api/cart/add

# 3. Adresse
curl -b /tmp/c.txt -H "Content-Type: application/json" \
  -d '{"street":"12 rue de la Paix","postalCode":"75001","city":"Paris","country":"FR","phone":"0145678901"}' \
  http://localhost:9080/api/checkout/address

# 4. Livraison
curl -b /tmp/c.txt -H "Content-Type: application/json" \
  -d '{"shippingMethod":"express"}' \
  http://localhost:9080/api/checkout/shipping

# 5. Paiement (CVV validé puis jeté)
curl -b /tmp/c.txt -H "Content-Type: application/json" \
  -d '{"cardHolder":"Alice","cardNumber":"4532015112830366","expiryMonth":"12","expiryYear":"2027","cvv":"123"}' \
  http://localhost:9080/api/checkout/payment

# 6. Validation finale
curl -b /tmp/c.txt -H "Content-Type: application/json" \
  -d "{\"securityToken\":\"$TOKEN\",\"items\":[{\"productId\":42,\"quantity\":2}]}" \
  http://localhost:9080/api/orders

Liens associés