API-Zugriff auf Bestellungen

Von Sascha Silbermann, 16. August 2026

Technische Dokumentation zur Bestell-Schnittstelle des Moduls gfs_api. Alle Endpoints operieren primär auf der xt:Commerce-Tabelle orders (mit Bezügen zu orders_products, orders_status_history, orders_total, address_book) und werden über die Domain-Klasse Order und ihre Sub-Objekte gekapselt.

Controller: src/Controller/OrderApiController.php Aggregat: src/Shop/Order/Order.php Sub-Objekte: OrderData, OrderAddress, OrderProduct, OrderProducts, OrderStatus, OrderStatusHistory, OrderStatusHistoryEntry
1. Endpoint-Übersicht
Methode Pfad Handler Permission
GET /rest/order/{id} getOrder read order
GET /rest/orders/new/{scope} getNewOrders read order
PATCH /rest/order/{id}/status updateOrderStatus edit order

{id} = orders.orders_id. {scope} = summary (Default) oder full.

Alle Endpoints setzen OAuth2-Auth voraus. 401 bei fehlendem Token, 403 bei fehlender Permission.

Wichtig: Bestellungen werden über die API nicht angelegt und nicht gelöscht — sie entstehen ausschließlich über den Shop-Bestellprozess und werden im xt:Commerce-Backend gepflegt.


2. Datenmodell 2.1 Struktur einer Bestellung (Order::toArray())

GET /rest/order/{id} liefert ein Aggregat-Objekt mit folgenden Top-Level-Feldern:

Feld Typ / Beschreibung
id orders.orders_id (int)
data OrderData-Objekt: Order-Kopfdaten (Kundenreferenz, Datum, Steuerinformationen, Status-ID, Total, ...)
customer Kunden-Snapshot zum Bestellzeitpunkt (Rechnungsempfänger; kann null sein, wenn Kunde inzwischen gelöscht)
deliveryAddress OrderAddress-Snapshot der Lieferadresse (aus orders-Feldern, nicht aus dem Adressbuch)
billingAddress OrderAddress-Snapshot der Rechnungsadresse
currency Verwendete Währung inkl. Wechselkurs (code, multiplicator)
payment Zahlart mit Sprach-Descriptions (payment.description[<langcode>])
bankAccount Bankverbindung des Bestellers, sofern hinterlegt (SEPA), sonst null
shipping Versandart mit Sprach-Descriptions (shipping.description[<langcode>])
products Liste der Positionen aus orders_products (Modell-Nr., Menge, Einzel-/Zeilenpreis, Steuersatz)
status Aktueller Bestellstatus (OrderStatus)
statusHistory Historie aus orders_status_history, indexiert per Zeitstempel

Die Adress- und Kunden-Snapshots werden beim Anlegen der Bestellung eingefroren. Änderungen am Kunden-Datensatz oder Adressbuch nach Bestelleingang wirken sich hier nicht mehr aus.

2.2 Feldbelegung OrderStatusHistoryEntry

Beim PATCH auf den Status wird intern ein Historieneintrag geschrieben (orders_status_history).

API-Feld DB-Spalte Typ Pflicht
orderId orders_id int ✔ (auto aus URL)
statusId orders_status_id int
dateAdded date_added datetime auto (Y-m-d H:i:s)
customerNotified customer_notified bool (0/1) Default false
comments comments string
changeTrigger change_trigger string Default api
customerShowComment customer_show_comment bool (0/1) Default false
callbackId callback_id string
callbackMessage callback_message string

Pflichtfelder (OrderStatusHistoryEntry::REQUIRED_FIELDS): orderId, statusId, dateAdded — die ersten beiden werden vom Controller gesetzt, dateAdded von OrderStatusHistory::addEntry().


3. Endpoints im Detail 3.1 GET /rest/order/{id} — einzelne Bestellung

Response 200:

{
  "id": 12345,
  "data":     { "customersId": 42, "datePurchased": "2026-08-14 10:15:03", ... },
  "customer": { "id": 42, "firstname": "Max", "lastname": "Mustermann", ... },
  "deliveryAddress": { "firstname": "Max", "streetName": "Musterstraße", ... },
  "billingAddress":  { "firstname": "Max", "streetName": "Musterstraße", ... },
  "currency": { "code": "EUR", "multiplicator": 1 },
  "payment":  { "paymentCode": "prepayment", "description": { "de": { ... } } },
  "shipping": { "shippingCode": "dhl_shipping", "description": { "de": { ... } } },
  "products": [ { "orderProductId": 98765, "productsModel": "12345", "quantity": 2, ... } ],
  "status":   { "id": 1, "name": "Offen", ... },
  "statusHistory": { "2026-08-14 10:15:03": { "statusId": 1, "changeTrigger": "system", ... } }
}

404 (Bestellung unbekannt):

{ "error": "Order not found", "code": 404, "customerId": 12345 }

Hinweis: Der Key customerId im 404-Body ist historisch — er enthält tatsächlich die angefragte orderId.


3.2 GET /rest/orders/new/{scope} — neue Bestellungen

Liefert alle Bestellungen mit Status = "neu". Der maßgebliche Status wird aus der Modul-Config order_status_new gelesen (im Admin-UI unter „GFS API → Einstellungen" konfigurierbar).

Parameter {scope}:

summary (Default) — schlanke Liste mit id und created. Keine Größenbegrenzung. full — vollständige Order-Objekte (wie in §3.1) für die letzten 10 Bestellungen (per orders_id DESC limitiert).

Cache-Verhalten: Der Controller ruft page_cache_kill_switch->trigger() auf — jeder Aufruf umgeht den Drupal-Page-Cache, damit die WaWi immer den aktuellen Stand sieht.

Response 200 (summary):

{
  "12347": { "id": 12347, "created": "2026-08-14 10:20:00" },
  "12346": { "id": 12346, "created": "2026-08-14 09:55:12" },
  "12345": { "id": 12345, "created": "2026-08-14 09:12:44" }
}

Format ist ein Objekt (nicht Array) mit orders_id als Schlüssel, umgekehrt sortiert.

Response 200 (full): wie summary, aber mit dem kompletten Order-Aggregat je Eintrag (siehe §3.1).

Fehler:

400 — scope ist weder summary noch full. 500 — Modul-Config order_status_new ist nicht gesetzt ("New order status not configured").
3.3 PATCH /rest/order/{id}/status — Status ändern

Setzt den neuen Status auf der Bestellung UND legt einen Historieneintrag an (atomar hintereinander; kein DB-Transaction-Wrapper).

Request-Body (JSON):

{
  "statusId": 3,
  "comments": "Versand mit DHL, Track-ID 00340...",
  "customerNotified": true,
  "customerShowComment": true,
  "changeTrigger": "wawi"
}
Pflicht: statusId (int) — muss ein gültiger Wert aus dem SystemStatus('order_status')-Katalog sein (xt:Commerce-orders_status). Optional: comments, customerNotified, customerShowComment, changeTrigger, callbackId, callbackMessage. dateAdded wird von OrderStatusHistory::addEntry() automatisch gesetzt.

Response 200:

{
  "message": "Order status updated successfully",
  "orderId": 12345,
  "newStatus": {
    "orderId": 12345,
    "statusId": 3,
    "dateAdded": "2026-08-16 14:22:31",
    "customerNotified": true,
    "comments": "Versand mit DHL, Track-ID 00340...",
    "changeTrigger": "wawi",
    "customerShowComment": true,
    "callbackId": "",
    "callbackMessage": ""
  }
}

Fehler:

400 — statusId fehlt ("Missing statusId in request body") 400 — statusId unbekannt ("Invalid order status ID", Response enthält statusId) 404 — Bestellung nicht gefunden 500 — Historieneintrag konnte nicht geschrieben werden oder Exception im Save

Wichtig zur Semantik:

Der Controller ändert orders.orders_status UND schreibt einen neuen orders_status_history-Datensatz. Beide Schritte laufen ohne gemeinsame Transaction. Bei einem Fehler im zweiten Schritt (History) wird der erste Schritt (Data-Set) auf orders.orders_status bereits in-Memory gehalten, aber nicht mehr persistiert (getData()->save() läuft nach dem addEntry). Konsumenten sollten bei einem 500 den neuen Ist-Zustand per GET verifizieren, bevor sie retryen. changeTrigger hat den Default api. Wer einen anderen Wert (wawi, admin, system) hinterlegen möchte, muss ihn im Body explizit senden. E-Mail-Benachrichtigung an den Kunden wird nicht automatisch ausgelöst — customerNotified ist nur ein Flag im Historieneintrag, keine Trigger-Aktion.
4. Verhalten und Grenzen im Überblick
Verhalten Ort
getNewOrders umgeht Page-Cache pro Request OrderApiController.php:72
scope=full ist auf 10 Bestellungen limitiert OrderApiController.php:92-94
Response-Objekt bei getNewOrders ist assoziativ nach orders_id, reverse-sortiert OrderApiController.php:102-104
Status-Update = 2 unabhängige DB-Writes (Order-Data + History) OrderApiController.php:170-179
changeTrigger-Default api überall wo nicht explizit anders gesetzt OrderStatusHistoryEntry.php:58
Kein POST / DELETE — Bestellungen werden nicht per API erzeugt oder gelöscht
Adressen und Kundenblock sind Snapshots zum Bestellzeitpunkt Order.php:271-283 (aus orders-Feldern)
getOrder liefert ein Objekt ohne Wrapper (new JsonResponse((object) $order->toArray())) OrderApiController.php:125

5. Statuscodes
Code Bedeutung
200 GET / PATCH erfolgreich
400 Ungültige Parameter (scope unbekannt, statusId fehlt oder ungültig)
401 Kein / ungültiges OAuth2-Token
403 Token hat nicht die erforderliche Permission (read order bzw. edit order)
404 Bestellung nicht gefunden — Response enthält die angefragte ID
500 Config fehlt (order_status_new) oder interner DB-/Serialisierungs-Fehler

6. Beispiel-Sequenz (curl)
TOKEN="..."   # OAuth2-Bearer-Token
BASE="https://api.gfs-topshop.de"

# 1) Neue Bestellungen (Kurzform) abrufen — WaWi-Polling
curl -H "Authorization: Bearer $TOKEN" \
     "$BASE/rest/orders/new/summary"

# 2) Details zu einer bestimmten Bestellung abrufen
curl -H "Authorization: Bearer $TOKEN" \
     "$BASE/rest/order/12345"

# 3) Status auf "In Bearbeitung" (2) setzen mit Kundenbenachrichtigung
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
     -d '{
       "statusId": 2,
       "comments": "Auftrag ist in Bearbeitung.",
       "customerNotified": true,
       "customerShowComment": true,
       "changeTrigger": "wawi"
     }' \
     "$BASE/rest/order/12345/status"

# 4) Bulk-Übernahme via WaWi (Polling-Pattern):
#    a) alle neuen Bestellungen abrufen
#    b) je Bestellung Details holen, in WaWi importieren
#    c) Status auf "übernommen" setzen -> Bestellung fällt aus new-Liste
Kategorie