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, OrderStatusHistoryEntry1. 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 OrderStatusHistoryEntryBeim 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 SaveWichtig 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