Technische Dokumentation zur Adressbuch-Schnittstelle des Moduls gfs_api. Alle Endpoints operieren auf der xt:Commerce-Tabelle customers_addresses und werden über die Domain-Klassen Customer, CustomerAddresses und CustomerAddress gekapselt.
Controller: src/Controller/CustomerApiController.php Collection: src/Shop/Customer/CustomerAddresses.php Entität: src/Shop/Customer/CustomerAddress.php1. Endpoint-Übersicht
| Methode | Pfad | Handler | Permission |
|---|---|---|---|
| GET | /rest/customer/{id}/addresses | getCustomerAddresses | read customer |
| GET | /rest/customer/{id}/address/{addressId} | getCustomerAddress | edit customer |
| POST | /rest/customer/{id}/address | createCustomerAddress | edit customer |
| PATCH | /rest/customer/{id}/address/{addressId} | updateCustomerAddress | edit customer |
| PATCH | /rest/customers/{id}/addresses | updateCustomerAddresses | edit customer |
| DELETE | /rest/customer/{id}/address/{addressId} | deleteCustomerAddress | edit customer |
{id} = xt:Commerce-Kundennummer (customers.customers_id), {addressId} = customers_addresses.address_book_id.
Alle Endpoints setzen OAuth2-Auth voraus (Standard des Moduls). 401 kommt bei fehlendem/abgelaufenem Token, 403 bei fehlender Permission.
2. Datenmodell 2.1 API-Feld ↔ DB-Spalte
| API-Feld | DB-Spalte | Typ | Pflicht |
|---|---|---|---|
| id | address_book_id | int | serverseitig |
| customerId | customers_id | int | ✔ (auto-gesetzt) |
| gender | customers_gender | string (m/f/"") | |
| birthday | customers_dob | string|null | |
| company | customers_company | string | |
| company2 | customers_company_2 | string | |
| company3 | customers_company_3 | string | |
| firstname | customers_firstname | string | ✔ |
| lastname | customers_lastname | string | ✔ |
| street | customers_street_address | string | (auto-berechnet aus streetName+streetNumber) |
| streetName | customers_address_street_name | string | ✔ |
| streetNumber | customers_address_street_no | string | ✔ |
| suburb | customers_suburb | string | |
| postcode | customers_postcode | string | ✔ |
| city | customers_city | string | ✔ |
| country | customers_country_code | string (ISO-alpha-2, z.B. DE) | ✔ |
| state | customers_federal_state_code | int|null | |
| phone | customers_phone | string | |
| fax | customers_fax | string | |
| mobile | customers_mobile_phone | string | |
| notice | bemerkung | string | |
| type | address_class | string | siehe §2.2 |
| default | address_default | int (0/1) | |
| created | date_added | string (Y-m-d H:i:s) | auto-gesetzt beim POST |
| modified | last_modified | string (Y-m-d H:i:s) | auto-gesetzt bei save |
Pflichtfelder (CustomerAddress::REQUIRED_FIELDS): customerId, firstname, lastname, city, country, postcode, streetName, streetNumber.
2.2 Adress-Typ (type) und Default-Kennzeichen type = 'default' → Rechnungsadresse. In customers_addresses gilt die Konvention: nur eine Adresse pro Kunde darf address_default = 1 tragen. Die Entität CustomerAddress::save() setzt vor dem eigenen UPDATE automatisch alle anderen Adressen des Kunden auf address_default = 0, wenn type = 'default' gesetzt wird. Vorsicht — die Konsistenz wird nur beim save()-Pfad garantiert, nicht bei direkten DB-Änderungen. Andere type-Werte (typischerweise 'shipping', '') sind Zusatz-/ Lieferadressen. 2.3 street vs. streetName/streetNumberBeim Konstruieren einer CustomerAddress wird street immer neu aus trim(streetName . ' ' . streetNumber) gebaut. Ein direkt gelieferter street-Wert wird also überschrieben. Für Änderungen immer streetName und streetNumber senden.
3. Endpoints im Detail 3.1 GET /rest/customer/{id}/addresses — alle Adressen
Response 200:
{
"addresses": {
"1001": { "id": 1001, "customerId": 42, "type": "default", ... },
"1002": { "id": 1002, "customerId": 42, "type": "shipping", ... }
},
"code": 200
}
Der äußere addresses-Wert ist ein Objekt (assoziativ) mit address_book_id als Schlüssel — nicht ein Array. toArray() in CustomerAddresses:166-172.
404 (Kunde unbekannt):
{ "error": "Customer not found", "code": 404, "customerId": 42 }
3.2 GET /rest/customer/{id}/address/{addressId} — einzelne Adresse
Response 200:
{
"address": { "id": 1001, "customerId": 42, ... },
"code": 200
}
404-Varianten:
Kunde unbekannt → { "error": "Customer not found", "customerId": 42, "code": 404 } Adresse existiert nicht bei diesem Kunden → { "error": "Address not found", "addressId": 1001, "code": 404 }Die Adresse muss dem Kunden gehören — es werden nur Adressen aus getAddresses() geliefert, d.h. Filter über customers_id (siehe CustomerAddresses::load()).
3.3 POST /rest/customer/{id}/address — neue Adresse
Request-Body (JSON): alle Pflichtfelder aus §2.1 setzen. customerId wird serverseitig auf {id} überschrieben; created/modified werden auto-gesetzt, wenn nicht mitgeliefert.
{
"type": "shipping",
"gender": "m",
"firstname": "Max",
"lastname": "Mustermann",
"streetName": "Musterstraße",
"streetNumber": "12a",
"postcode": "12345",
"city": "Musterstadt",
"country": "DE"
}
Response 201:
{
"message": "Address created successfully",
"address": { "id": 1003, "customerId": 42, ... },
"code": 201
}
Fehler:
404 wenn Kunde nicht existiert. 500 bei DB-Fehler; Exception-Message wird durchgereicht ("Failed to create address: ..."). Häufigste Ursache: fehlende Pflichtfelder oder Constraint-Violations in customers_addresses.3.4 PATCH /rest/customer/{id}/address/{addressId} — Adresse ändern
Request-Body (JSON, alle Felder optional): nur die zu ändernden Felder senden. Unbekannte Schlüssel werden verworfen (Filter über array_key_exists($key, $address->getProperties())).
{
"streetName": "Neue Straße",
"streetNumber": "5",
"postcode": "54321"
}
modified wird auto-gesetzt. Ein type = "default"-Update triggert das in §2.2 beschriebene Reset-Update aller anderen Adressen — der Kunde hat danach garantiert genau eine Default-Adresse.
Response 200:
{
"message": "Address updated successfully",
"address": { "id": 1001, ... },
"code": 200
}
Fehler: 404 (Kunde/Adresse), 500 (DB).
3.5 PATCH /rest/customers/{id}/addresses — Bulk-Replace ⚠
WICHTIG — destruktive Semantik: Anders als der PATCH auf eine einzelne Adresse ist dieser Endpoint kein partieller Merge, sondern ein kompletter Ersatz aller Adressen des Kunden. CustomerAddresses::setEntities() löscht zuerst ALLE bestehenden Zeilen aus customers_addresses (per DELETE ... WHERE customers_id = {id}) und legt anschließend die übergebenen Adressen neu an.
Bei id-Angabe im Request wird die ursprüngliche address_book_id beibehalten, sofern sie vor dem Delete existierte — sonst vergibt MySQL eine neue.
Request-Body: JSON-Array von Adress-Objekten (jeweils wie §3.3).
[
{ "id": 1001, "type": "default", "firstname": "Max", ... },
{ "id": 1002, "type": "shipping", "firstname": "Max", ... },
{ "type": "shipping", "firstname": "Max", ... }
]
Response 200:
{
"message": "Addresses updated successfully",
"addresses": { "1001": {...}, "1002": {...}, "1003": {...} },
"code": 200
}
Empfohlen: Diesen Endpoint nur nutzen, wenn die WaWi/das externe System den vollständigen Adressbestand konsistent hält. Für inkrementelle Änderungen ist der Einzel-PATCH (§3.4) sicherer.
3.6 DELETE /rest/customer/{id}/address/{addressId} — Adresse löschen
Löscht die Adresse aus customers_addresses (DELETE ... WHERE customers_id = {id} AND address_book_id = {addressId}) und entfernt sie aus der In-Memory-Collection (CustomerAddresses::deleteAddress()).
Response 200:
{ "message": "Address deleted successfully", "code": 200 }
Fehler: 404 (Kunde/Adresse), 500 (DB).
Nicht abgefangen (bekannte Einschränkung):
Löschen der aktuellen Default-Adresse hinterlässt einen Kunden ohne Default-Adresse. Konsumenten sollten in diesem Fall selbst eine neue Default setzen (§3.4 mit type = "default"). Referenzielle Integrität zu Bestellungen: orders-Zeilen halten Adressen als Snapshot separat, ein Löschen einer Adresse beeinträchtigt bestehende Bestellungen nicht.4. Verhalten und Grenzen im Überblick
| Verhalten | Ort |
|---|---|
| customerId wird beim POST immer auf den Path-Parameter überschrieben | CustomerApiController.php:222 |
| created und modified werden auto-gesetzt (Y-m-d H:i:s) | CustomerApiController.php:216-221, CustomerAddress.php:216-233 |
| Nur bekannte Felder aus EDITABLE_FIELDS werden geschrieben | CustomerAddress.php:164-190, CustomerApiController.php:281-285 |
| street wird immer aus streetName + streetNumber gebaut | CustomerAddress.php:203-209 |
| Genau eine Adresse pro Kunde darf default = 1 sein — type=default triggert Reset | CustomerAddress.php:258-266 |
| Bulk-PATCH löscht vorher ALLE Adressen des Kunden | CustomerAddresses.php:134-144 |
| Adressen werden bei Kundenanlage nicht implizit erzeugt | — |
5. Statuscodes
| Code | Bedeutung |
|---|---|
| 200 | GET / PATCH / DELETE erfolgreich |
| 201 | POST erfolgreich (neue Adresse angelegt) |
| 401 | Kein/ungültiges OAuth2-Token |
| 403 | Token hat nicht die erforderliche Permission (read customer bzw. edit customer) |
| 404 | Kunde oder Adresse nicht gefunden — Response enthält customerId bzw. addressId |
| 500 | Interner Fehler beim Schreiben/Löschen — Response enthält Exception-Message unter error |
6. Beispiel-Sequenz (curl)
TOKEN="..." # OAuth2-Bearer-Token
BASE="https://api.gfs-topshop.de"
CID=42
# 1) Alle Adressen holen
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/rest/customer/$CID/addresses"
# 2) Neue Lieferadresse anlegen
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"type": "shipping",
"firstname": "Max", "lastname": "Mustermann",
"streetName": "Musterstraße", "streetNumber": "12a",
"postcode": "12345", "city": "Musterstadt", "country": "DE"
}' \
"$BASE/rest/customer/$CID/address"
# 3) Adresse aktualisieren (nur geänderte Felder)
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "streetNumber": "12b" }' \
"$BASE/rest/customer/$CID/address/1003"
# 4) Adresse löschen
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
"$BASE/rest/customer/$CID/address/1003"