Skip to content

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 codes
  • OrderController (/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

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

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_phone
  • checkout_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

{ "shippingMethod": "express" }
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

{ "code": "PROMO10" }

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

{
  "canCheckout": true,
  "authenticated": true,
  "hasToken": true,
  "message": "Access authorized"
}
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

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

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