API — Orders and checkout¶
This page documents the entire PerfShop checkout journey, which unfolds in 5 steps spread over two controllers.
Controllers covered
CheckoutController(/api/checkout/*) → steps 2 to 4 (address, shipping, payment) + promo codesOrderController(/api/orders/*) → step 5 (final validation) + consultation, cancellation, invoice
Step 1 (login) is documented in auth.md.
The 5 checkout journey steps¶
sequenceDiagram
autonumber
participant F as React Frontend
participant B as Backend
participant DB as MySQL
Note over F: Cart filled
F->>B: POST /api/auth/login
B-->>F: 200 { securityToken, scripting tokens }
F->>B: POST /api/checkout/address
Note right of B: Strict validation<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 discarded immediately<br/>CVV never logged
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 }
Each step locks the next: attempting POST /api/checkout/payment without first submitting the address returns a 400 Bad Request. Intermediate states are stored in the HttpSession under the keys checkout_address, checkout_shipping, etc.
Scripting chaos control¶
The checkout journey is the only path protected by Scripting chaos. Each step validates anti-replay headers whose list and complexity depend on the active level:
| Level | Name | Headers required at each step |
|---|---|---|
| 0 | Disabled | None |
| 1 | Junior | X-Session-Token, X-Request-ID |
| 2 | Intermediate | + X-Action-Token (renewed at each step) |
| 3 | Expert | + X-CSRF-Token, X-Step-Token, X-Signature (HMAC) |
| 4 | Maestro | Same + HMAC key derived from sessionToken |
Algorithm details are in Scripting chaos. In the examples below, we stay at level 0 (no headers).
CheckoutController¶
POST /api/checkout/address¶
Submits the shipping address. This is step 2 of the journey (after login).
Auth: user session + valid securityToken
Controller: CheckoutController.submitAddress()
Validation: ValidationService.validateProfile()
Request¶
{
"street": "12 rue de la Paix",
"postalCode": "75001",
"city": "Paris",
"region": "Île-de-France",
"country": "FR",
"phone": "0145678901"
}
| Field | Type | Required | Constraint |
|---|---|---|---|
street |
string | yes | 5–200 characters |
postalCode |
string | yes | National format (FR: 5 digits) |
city |
string | yes | 2–100 characters, no rejected accents |
region |
string | no | 2–100 characters |
country |
string | yes | ISO 3166-1 alpha-2 code |
phone |
string | yes | National format, FR mobile numbers 06/07 rejected for delivery |
Response — 200 OK¶
In Scripting chaos level ≥ 1, the response also includes rotation headers (X-Session-Token, etc.) to reuse for the next step.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing mandatory field (keys checkout.error.*_required) |
| 401 | Not logged in (key auth.error.not_authenticated) |
| 422 | Business validation failed (phone format, postal code, country). Body: {"error": "Invalid data", "fields": {...}} |
Session side effects¶
The full address is serialized and stored in HttpSession under:
checkout_address(formatted concatenation)checkout_city,checkout_country,checkout_phonecheckout_street,checkout_postalCode,checkout_region
These values are re-read by OrderController.createOrder() at the final step.
POST /api/checkout/shipping¶
Selects the shipping method. Step 3.
Auth: session + securityToken
Prerequisite: checkout_address present in session
Request¶
| Value | Description |
|---|---|
standard |
Standard delivery (3-5 days) |
express |
Fast delivery (1-2 days) |
premium |
Next-day delivery before 1pm |
Response — 200 OK¶
{
"success": true,
"step": "shipping",
"nextStep": "payment",
"shippingMethod": "express",
"deliveryDays": 2,
"deliveryLabel": "2 business day(s)",
"workingDays": true
}
A7 — Calendar days instead of business days
In Business chaos level 2 and above, the delivery delay calculation switches to calendar days instead of business days. Weekends and holidays are no longer excluded. A "standard 3 business days" delivery may fall on a Sunday. The deliveryDays, deliveryLabel and workingDays fields reflect the corrupted calculation. See A7.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Invalid shipping method (key checkout.error.shipping_invalid) |
| 400 | Address not submitted (key checkout.error.address_first) |
| 401 | Not logged in |
POST /api/checkout/payment¶
Submits payment information. Step 4.
Auth: session + securityToken
Prerequisite: checkout_address and checkout_shipping present in session
Validation: ValidationService.validateCard() (Luhn, expiry) + validateCvv()
Request¶
{
"cardHolder": "Alice Durand",
"cardNumber": "4532015112830366",
"expiryMonth": "12",
"expiryYear": "2027",
"cvv": "123"
}
| Field | Validation |
|---|---|
cardHolder |
Not empty |
cardNumber |
16 digits + Luhn algorithm |
expiryMonth |
1–12 |
expiryYear |
Future, ≤ 2029 |
cvv |
3 digits (or 4 for Amex) |
CVV security
The CVV is validated locally by ValidationService.validateCvv() then immediately discarded. It is:
- never stored in the session
- never logged, neither in clear text nor hashed
- never transmitted to
OrderService
Only a boolean cvvVerified flag is propagated to the next step, along with the last 4 digits of the card (cardLast4) for the confirmation display. This is the expected production behavior — PerfShop implements it correctly despite its pedagogical nature.
Response — 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"
}
}
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing previous steps (address or shipping) |
| 401 | Not logged in |
| 422 | Invalid card (Luhn, expiry, format) or invalid CVV |
The body of 422 responses follows the structure {"error": "...", "field": "..."}, for example {"error": "Invalid card number (Luhn check failed)", "field": "cardNumber"}.
POST /api/checkout/promo¶
Validates a promo code and returns the applicable discount percentage.
Auth: session + securityToken
Request¶
Response — 200 OK (valid code)¶
{
"valid": true,
"code": "PROMO10",
"discountPercent": 10,
"message": "Valid promo code: PROMO10 -> 10% discount"
}
Response — 200 OK (invalid code, nominal behavior)¶
{
"valid": false,
"code": "NONEXISTENT",
"discountPercent": 0,
"message": "Invalid promo code: NONEXISTENT"
}
A6 — Invalid code silently accepted
In Business chaos level 2 and above, an invalid promo code is accepted with valid: true and discountPercent: 0. The user sees "Promo code applied" but gets no discount. No error message is returned — the anomaly is only visible by comparing the total before/after. See A6.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing code (checkout.error.promo_required) |
| 401 | Not logged in |
GET /api/checkout/session¶
Returns the current checkout session state — useful to rehydrate a cart abandoned mid-journey.
Auth: session + securityToken
Response — 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"
}
Fields not yet filled are null.
OrderController¶
GET /api/orders/checkout/verify¶
Checks that the current user can access the checkout journey. Used by the frontend to decide whether to redirect to login.
Auth: session (optional — returns status in all cases)
Response — 200 OK¶
| Field | Description |
|---|---|
canCheckout |
true if authenticated and hasToken |
authenticated |
Active user session |
hasToken |
Checkout securityToken still valid |
POST /api/orders¶
Step 5 — final order validation. Creates the order in the database, decrements stock, sends confirmation, and (if applicable) injects the pedagogical agent code.
Auth: user session + valid securityToken in the body
Controller: OrderController.createOrder()
Service: OrderService.createOrderFromItems()
Request¶
{
"securityToken": "a7f1c2e8-4b9d-4f11-9c8e-1a2b3c4d5e6f",
"items": [
{ "productId": 42, "quantity": 2 }
],
"shippingAddress": "12 rue de la Paix, 75001 Paris",
"shippingMethod": "express",
"paymentMethod": "CARD"
}
| Field | Type | Required | Note |
|---|---|---|---|
securityToken |
string (UUID) | yes | Generated by POST /api/auth/login |
items[] |
array | yes | At least one item |
items[].productId |
Long | yes | |
items[].quantity |
Integer | yes | ≥ 1 |
items[].unitPrice |
Decimal | no | Ignored under nominal behavior — see S5 |
shippingAddress |
string | no | Re-read from session if absent |
shippingMethod |
string | no | Re-read from session if absent |
paymentMethod |
string | no | CARD by default |
If address/shipping/payment fields are absent from the body, the controller re-reads them from the session (set in previous steps). This allows a strict step-by-step flow OR a "one-shot" flow where the frontend sends everything at once.
Request headers¶
| Header | Usage |
|---|---|
X-Student-Token |
Optional — if present, the associated pedagogical agent code is injected in the response |
| Scripting chaos headers | Depending on the active level |
Response — 201 Created¶
With a valid X-Student-Token associated with a pedagogical session, the response also includes agentCode:
{
"success": true,
"orderNumber": "PS-2026-000123",
"orderId": 789,
"totalAmount": 299.98,
"agentCode": "NEPTUNE-1284"
}
The agentCode is computed by PedagogiqueSessionService.getAgentCodeForToken() according to the active BAC level. See Dynamic agent code.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Cart empty (key order.error.cart_empty) |
| 401 | Not authenticated — also returns redirectTo: /login |
| 403 | Invalid token — also returns redirectTo: /login |
| 500 | Server error or active functional chaos |
S4 — Stored XSS via shippingAddress
In Security chaos level 2 and above, the shippingAddress field is stored without HTML escaping. A payload <script>alert(1)</script> is persisted as-is in the database and re-executed in the admin interface displaying order details. See S4.
S5 — Client-side price tampering
In Security chaos level 2 and above, the unitPrice body field is accepted: if the user sends {"productId": 42, "quantity": 1, "unitPrice": 0.01}, the order is recorded at 1 cent. The difference between the client price and the actual price in the database is logged but does not block the order. See S5.
GET /api/orders¶
Lists the logged-in user's orders, sorted from most recent to oldest.
Auth: session
Controller: OrderController.getUserOrders()
Response — 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
}
| Field | Description |
|---|---|
orders[] |
List of orders |
historyTotal |
Cumulative total of all orders |
Active chaos¶
DB deadlock possible
The endpoint calls orderService.applyDeadlockChaosPublic() before the read — in active Performance chaos, a deadlock may raise a 500 Internal Server Error with the message Database deadlock detected — transaction rolled back.
A15 — Silent history corruption
In Business chaos level 4, the totalAmount values are multiplied by 1.1 at display without touching the database values. The historyTotal field follows the same corruption. An order at 100 € appears at 110 € in the list but GET /api/orders/{id} returns the correct amount if loaded from the DB. See A15.
A10 — Wrong order history total
In Business chaos level 3, historyTotal is calculated with a per-line cumulative rounding — the result differs by a few cents from the actual sum.
GET /api/orders/{id}¶
Returns an order's information.
Auth: session — the user can only see their own orders
Response — 200 OK¶
The Order entity serialized with relationships (user, items).
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Order belonging to another user (key order.error.access_denied) |
| 404 | Order not found |
S2 — Order IDOR
In Security chaos level 1 and above, the ownership check is disabled. User A can read user B's orders by guessing or enumerating IDs. Cross-user access is logged (recordIdorAccess) but is not blocked — the order is returned normally. See S2.
GET /api/orders/{id}/details¶
Enriched version of GET /api/orders/{id} — returns order lines with product names, quantities, prices and subtotals, in a flattened structure suited to display.
Auth: session (same IDOR rule as above)
Response — 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": "Bluetooth audio headset",
"quantity": 2,
"unitPrice": 149.99,
"subtotal": 299.98
}
]
}
Error codes¶
Identical to GET /api/orders/{id}. S2 (IDOR) applies here too.
GET /api/orders/{id}/invoice¶
Simulated invoice generation endpoint. Used in teaching to illustrate S8 (Path Traversal).
Auth: session
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
format (query) |
string | pdf |
Invoice format: pdf or csv |
Nominal behavior (Security level 0-2)¶
Strictly validates format ∈ {"pdf", "csv"}. Returns a simulated content:
{
"orderId": 789,
"format": "pdf",
"content": "INVOICE #789\nPerfShop SAS — 123 rue du Commerce — 75001 Paris\nThank you for your order."
}
Error codes (nominal)¶
| Code | Cause |
|---|---|
| 400 | Invalid format — only pdf and csv are allowed (key order.error.format_invalid) |
| 401 | Not authenticated |
S8 — Path Traversal
In Security chaos level 3 and above, the format parameter is no longer validated. A student can pass format=../../../etc/passwd and the simulation returns content marked "SIMULATED PATH TRAVERSAL":
{
"orderId": 789,
"format": "/etc/passwd",
"content": "root:x:0:0:root:/root:/bin/bash\n...",
"warning": "SIMULATED PATH TRAVERSAL — content is not real (pedagogical)"
}
No real file on the host system is read — the content is a static simulation, but the code path demonstrates the vulnerability. See S8.
POST /api/orders/{id}/cancel¶
Cancels an order. Allowed only on PENDING or CONFIRMED statuses.
Auth: session — strict ownership check
Response — 200 OK¶
{
"success": true,
"message": "Order cancelled successfully",
"orderNumber": "PS-2026-000123",
"status": "CANCELLED"
}
Side effects — restocking¶
Under nominal behavior, cancellation re-credits stock for each item back to the parent product (UPDATE products.stock += items[].quantity).
Error codes¶
| Code | Cause |
|---|---|
| 400 | Non-cancellable status — {"error": "Cannot cancel an order with status SHIPPED", "status": "SHIPPED"} |
| 401 | Not authenticated |
| 403 | Another user's order |
| 404 | Order not found |
A16 — Cancel without restocking
In Business chaos level 4, restocking is removed: the order does move to CANCELLED but stock is not replenished. Items are silently "lost". No error returned. See A16.
curl example — full journey¶
# 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. Add to cart
curl -b /tmp/c.txt -H "Content-Type: application/json" \
-d '{"productId":42,"quantity":2}' \
http://localhost:9080/api/cart/add
# 3. Address
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. Shipping
curl -b /tmp/c.txt -H "Content-Type: application/json" \
-d '{"shippingMethod":"express"}' \
http://localhost:9080/api/checkout/shipping
# 5. Payment (CVV validated then discarded)
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. Final validation
curl -b /tmp/c.txt -H "Content-Type: application/json" \
-d "{\"securityToken\":\"$TOKEN\",\"items\":[{\"productId\":42,\"quantity\":2}]}" \
http://localhost:9080/api/orders
Related links¶
auth.md— login andsecurityTokengenerationcart.md— cart construction upstream- Security chaos — S2, S4, S5, S8
- Business chaos — A6, A7, A10, A15, A16
- Scripting chaos — checkout header protection
- Pedagogical agent code — injection in the order response