> For the complete documentation index, see [llms.txt](https://docs.codegiganten.de/plugin-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.codegiganten.de/plugin-documentation/apps/enterprise-loyalty-bonus-programm/api-zugriff.md).

# API Zugriff

Diese Dokumentation beschreibt die verfügbaren API-Endpunkte für das Loyalty Plugin.

## Authentifizierung und Berechtigungen

Alle Endpunkte erfordern Authentifizierung (`auth_required: true`). Verwenden Sie die Shopware API-Authentifizierung mit OAuth oder einem API-Key.

Zusätzlich wird seit Version 2.6.8 eine ACL-Berechtigung geprüft:

| Zugriff                        | Erforderliches Privileg |
| ------------------------------ | ----------------------- |
| Lesende Endpunkte (`GET`)      | `customer:read`         |
| Schreibende Endpunkte (`POST`) | `customer:update`       |

{% hint style="warning" %}
**Wichtig für bestehende Anbindungen:** Eine Integration bzw. Admin-Rolle, die diese Privilegien nicht besitzt, erhält seit 2.6.8 einen `403 Forbidden`. Hinterlegen Sie für Integrationen entweder das Flag „Administrator" oder eine ACL-Rolle, die Kunden lesen (`customer.viewer`) bzw. bearbeiten (`customer.editor`) darf. Die Rollen `customer.viewer` und `customer.editor` schließen automatisch auch die Loyalty-Entitäten mit ein.
{% endhint %}

## Kundenprofil abrufen

Ruft ein einzelnes Kundenprofil mit allen Loyalty-Informationen ab.

### Endpunkt

```http
GET /api/cogi/loyalty/customer-profile/{customerId}
```

### Parameter

| Parameter        | Typ            | Pflicht | Beschreibung                                                                                                                                                                                                                              |
| ---------------- | -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`     | string (path)  | Ja      | Die ID des Kunden                                                                                                                                                                                                                         |
| `salesChannelId` | string (query) | Nein    | Schränkt auf das Profil dieses Sales Channels ein. Existiert kein kanalspezifisches Profil, wird automatisch auf das globale Profil (`salesChannelId = null`) zurückgefallen. Ohne Angabe wird der Sales Channel des Kunden herangezogen. |

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "data": {
    "id": "0x...",
    "customerId": "0x...",
    "points": 500,
    "pointsExpirationDate": "2025-12-31",
    "customFieldRewards": null,
    "setBirthday": true,
    "newsletter": false,
    "active": true,
    "blocked": false,
    "optInConfirmedAt": "2024-01-15 10:30:00",
    "optOutAt": null,
    "birthday": "1985-04-12",
    "lastPointsReceived": "2024-01-20 14:30:00",
    "lastOrderDate": "2024-01-20 09:12:44",
    "createdAt": "2024-01-15 10:30:00",
    "updatedAt": "2024-01-20 15:45:00",
    "level": {
      "id": "0x...",
      "name": "Silver",
      "minPoints": 100,
      "tagId": "0x...",
      "multiplier": 1.5,
      "color": "#FF5733"
    },
    "previousLevel": {
      "id": "0x...",
      "name": "Bronze",
      "minPoints": 0,
      "tagId": "0x...",
      "multiplier": 1.0,
      "color": "#C0C0C0",
      "pointsToFallDown": 400
    },
    "nextLevel": {
      "id": "0x...",
      "name": "Gold",
      "minPoints": 1000,
      "tagId": "0x...",
      "multiplier": 2.0,
      "color": "#FFD700",
      "pointsToReach": 500
    },
    "customer": {
      "id": "0x...",
      "email": "customer@example.com",
      "firstName": "Max",
      "lastName": "Mustermann",
      "customerNumber": "CUST-001",
      "birthday": "1985-04-12",
      "salutationId": "0x...",
      "salutation": {
        "id": "0x...",
        "salutationKey": "mr",
        "displayName": "Herr",
        "letterName": "Sehr geehrter Herr"
      },
      "createdAt": "2023-06-01 09:15:00",
      "updatedAt": "2024-01-20 14:30:00",
      "newsletter": true,
      "salesChannelId": "0x...",
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    },
    "salesChannelId": "0x...",
    "salesChannel": {
      "id": "0x...",
      "name": "Storefront"
    }
  }
}
```

**Status: 404 Not Found** (wenn Profil nicht gefunden)

```json
{
  "status": "error",
  "message": "Customer profile not found."
}
```

## Kundenprofile abrufen (Liste)

Ruft mehrere Kundenprofile mit Filter- und Paginations-Optionen ab.

### Endpunkt

```http
GET /api/cogi/loyalty/customer-profiles
```

### Query-Parameter

| Parameter                | Typ                      | Pflicht | Beschreibung                                                                                                                                            |
| ------------------------ | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerIds`            | string (comma-separated) | Nein    | Filter nach Kunden-ID(s), kommagetrennt                                                                                                                 |
| `levelId`                | string                   | Nein    | Filter nach Level-ID                                                                                                                                    |
| `salesChannelId`         | string (comma-separated) | Nein    | Filter nach Sales Channel ID(s) des **Loyalty-Profils**, kommagetrennt (globale Profile haben `salesChannelId = null` und werden daher nicht getroffen) |
| `customerSalesChannelId` | string (comma-separated) | Nein    | Filter nach Sales Channel ID(s) des **Kundenkontos** (`customer.salesChannelId`), kommagetrennt                                                         |
| `minPoints`              | integer                  | Nein    | Mindestanzahl Punkte                                                                                                                                    |
| `maxPoints`              | integer                  | Nein    | Maximale Anzahl Punkte                                                                                                                                  |
| `limit`                  | integer                  | Nein    | Anzahl Ergebnisse (Standard: 20)                                                                                                                        |
| `offset`                 | integer                  | Nein    | Offset für Pagination (Standard: 0)                                                                                                                     |
| `sortBy`                 | string                   | Nein    | Sortierfeld (Standard: 'points')                                                                                                                        |
| `sortDirection`          | string                   | Nein    | Sortierrichtung: 'ASC' oder 'DESC' (Standard: 'DESC')                                                                                                   |

### Beispiel-Requests

**Alle Profile mit mindestens 100 Punkten:**

```http
GET /api/cogi/loyalty/customer-profiles?minPoints=100
```

**Profile bestimmter Kunden:**

```http
GET /api/cogi/loyalty/customer-profiles?customerIds=0x...,0x...,0x...
```

**Profile eines bestimmten Levels:**

```http
GET /api/cogi/loyalty/customer-profiles?levelId=0x...
```

**Alle Profile von Kunden eines bestimmten Verkaufskanals (unabhängig vom Profil-Scope, inkl. globaler Profile):**

```http
GET /api/cogi/loyalty/customer-profiles?customerSalesChannelId=0x...
```

**Nur kanalgebundene Profile dieses Kanals (Profil-Scope):**

```http
GET /api/cogi/loyalty/customer-profiles?salesChannelId=0x...
```

**Paginierte Ergebnisse:**

```http
GET /api/cogi/loyalty/customer-profiles?limit=50&offset=100
```

**Sortiert nach Punkten (aufsteigend):**

```http
GET /api/cogi/loyalty/customer-profiles?sortBy=points&sortDirection=ASC
```

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "data": [
    {
      "id": "0x...",
      "customerId": "0x...",
      "points": 500,
      "pointsExpirationDate": "2025-12-31",
      "customFieldRewards": null,
      "setBirthday": true,
      "newsletter": false,
      "active": true,
      "blocked": false,
      "optInConfirmedAt": "2024-01-15 10:30:00",
      "optOutAt": null,
      "birthday": "1985-04-12",
      "lastPointsReceived": "2024-01-20 14:30:00",
      "lastOrderDate": "2024-01-20 09:12:44",
      "createdAt": "2024-01-15 10:30:00",
      "updatedAt": "2024-01-20 15:45:00",
      "level": {
        "id": "0x...",
        "name": "Silver",
        "minPoints": 100,
        "tagId": "0x...",
        "multiplier": 1.5,
        "color": "#FF5733"
      },
      "previousLevel": {
        "id": "0x...",
        "name": "Bronze",
        "minPoints": 0,
        "tagId": "0x...",
        "multiplier": 1.0,
        "color": "#C0C0C0",
        "pointsToFallDown": 400
      },
      "nextLevel": {
        "id": "0x...",
        "name": "Gold",
        "minPoints": 1000,
        "tagId": "0x...",
        "multiplier": 2.0,
        "color": "#FFD700",
        "pointsToReach": 500
      },
      "customer": {
        "id": "0x...",
        "email": "customer@example.com",
        "firstName": "Max",
        "lastName": "Mustermann",
        "customerNumber": "CUST-001",
        "birthday": "1985-04-12",
        "salutationId": "0x...",
        "salutation": {
          "id": "0x...",
          "salutationKey": "mr",
          "displayName": "Herr",
          "letterName": "Sehr geehrter Herr"
        },
        "createdAt": "2023-06-01 09:15:00",
        "updatedAt": "2024-01-20 14:30:00",
        "newsletter": true,
        "salesChannelId": "0x...",
        "salesChannel": {
          "id": "0x...",
          "name": "Storefront"
        }
      },
      "salesChannelId": "0x...",
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    }
  ],
  "total": 150,
  "limit": 20,
  "offset": 0
}
```

## Kundentransaktionen abrufen

Ruft alle Transaktionen eines bestimmten Kunden ab.

### Endpunkt

```http
GET /api/cogi/loyalty/customer-transactions/{customerId}
```

### Parameter

| Parameter    | Typ           | Pflicht | Beschreibung      |
| ------------ | ------------- | ------- | ----------------- |
| `customerId` | string (path) | Ja      | Die ID des Kunden |

### Query-Parameter

| Parameter       | Typ     | Pflicht | Beschreibung                                                                                                                                            |
| --------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `received`      | boolean | Nein    | Filter nach empfangenen Transaktionen (true/false)                                                                                                      |
| `type`          | string  | Nein    | Filter nach Transaktionstyp (earn, spend, refund\_spend, refund\_earn, expire, manual\_add, manual\_deduct, api\_add, api\_deduct, interval\_deduction) |
| `orderId`       | string  | Nein    | Filter nach Bestell-ID                                                                                                                                  |
| `minValue`      | integer | Nein    | Mindestpunktwert                                                                                                                                        |
| `maxValue`      | integer | Nein    | Maximalpunktwert                                                                                                                                        |
| `limit`         | integer | Nein    | Anzahl Ergebnisse (Standard: 20)                                                                                                                        |
| `offset`        | integer | Nein    | Offset für Pagination (Standard: 0)                                                                                                                     |
| `sortBy`        | string  | Nein    | Sortierfeld (Standard: 'createdAt')                                                                                                                     |
| `sortDirection` | string  | Nein    | Sortierrichtung: 'ASC' oder 'DESC' (Standard: 'DESC')                                                                                                   |

### Beispiel-Requests

**Alle Transaktionen eines Kunden:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...
```

**Nur empfangene Transaktionen:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?received=true
```

**Transaktionen einer bestimmten Bestellung:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?orderId=0x...
```

**Nur positive Transaktionen (Gutschriften):**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?minValue=1
```

**Nur negative Transaktionen (Abzüge):**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?maxValue=-1
```

**Paginierte Ergebnisse:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?limit=50&offset=100
```

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "data": [
    {
      "id": "0x...",
      "customerId": "0x...",
      "orderId": "0x...",
      "salesChannelId": "0x...",
      "value": 100,
      "description": "Punkte für Bestellung #12345",
      "type": "earn",
      "received": true,
      "sourceTransactionId": null,
      "expirationDate": "2025-12-31 23:59:59",
      "createdAt": "2024-01-15 10:30:00",
      "updatedAt": "2024-01-15 10:30:00",
      "order": {
        "id": "0x...",
        "orderNumber": "12345",
        "amountTotal": 100.00,
        "currency": "EUR"
      },
      "customer": {
        "id": "0x...",
        "email": "customer@example.com",
        "firstName": "Max",
        "lastName": "Mustermann",
        "customerNumber": "CUST-001"
      },
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    },
    {
      "id": "0x...",
      "customerId": "0x...",
      "orderId": null,
      "salesChannelId": "0x...",
      "value": -50,
      "description": "Manueller Abzug",
      "type": "manual_deduct",
      "received": false,
      "sourceTransactionId": null,
      "expirationDate": null,
      "createdAt": "2024-01-20 14:15:00",
      "updatedAt": "2024-01-20 14:15:00",
      "order": null,
      "customer": {
        "id": "0x...",
        "email": "customer@example.com",
        "firstName": "Max",
        "lastName": "Mustermann",
        "customerNumber": "CUST-001"
      },
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    }
  ],
  "total": 45,
  "limit": 20,
  "offset": 0
}
```

## Punkte hinzufügen

Fügt Punkte für einen bestimmten Kunden hinzu. Nur positive Werte sind erlaubt.

### Endpunkt

```http
POST /api/cogi/loyalty/add-points
```

### Request Body

| Parameter        | Typ                   | Pflicht | Beschreibung                                                                                                                                                               |
| ---------------- | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`     | string (UUID)         | Ja      | Die ID des Kunden                                                                                                                                                          |
| `points`         | integer               | Ja      | Anzahl der Punkte (muss positiv sein, größer als 0)                                                                                                                        |
| `description`    | string                | Nein    | Beschreibung der Transaktion (Standard: "Manuell gesetzt via API")                                                                                                         |
| `salesChannelId` | string (UUID) \| null | Nein    | ID des Sales Channels. Wenn angegeben, muss die ID einen existierenden Sales Channel bezeichnen. Bei `null` oder Weglassen gilt die Konfiguration für alle Verkaufskanäle. |

### Beispiel-Request

**Punkte hinzufügen:**

```json
{
  "customerId": "0x...",
  "points": 100,
  "description": "Bonus für Kundenfeedback",
  "salesChannelId": "0x..."
}
```

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "message": "Points set successfully.",
  "transactionId": "0x...",
  "data": {
    "id": "0x...",
    "customerId": "0x...",
    "points": 550,
    "pointsExpirationDate": "2025-12-31",
    "customFieldRewards": null,
    "setBirthday": true,
    "newsletter": false,
    "active": true,
    "blocked": false,
    "optInConfirmedAt": "2024-01-15 10:30:00",
    "optOutAt": null,
    "birthday": "1985-04-12",
    "lastPointsReceived": "2024-01-21 10:00:00",
    "lastOrderDate": "2024-01-20 09:12:44",
    "createdAt": "2024-01-15 10:30:00",
    "updatedAt": "2024-01-21 10:00:00",
    "level": {
      "id": "0x...",
      "name": "Silver",
      "minPoints": 100,
      "tagId": "0x...",
      "multiplier": 1.5,
      "color": "#FF5733"
    },
    "previousLevel": {
      "id": "0x...",
      "name": "Bronze",
      "minPoints": 0,
      "tagId": "0x...",
      "multiplier": 1.0,
      "color": "#C0C0C0",
      "pointsToFallDown": 450
    },
    "nextLevel": {
      "id": "0x...",
      "name": "Gold",
      "minPoints": 1000,
      "tagId": "0x...",
      "multiplier": 2.0,
      "color": "#FFD700",
      "pointsToReach": 450
    },
    "customer": {
      "id": "0x...",
      "email": "customer@example.com",
      "firstName": "Max",
      "lastName": "Mustermann",
      "customerNumber": "CUST-001",
      "birthday": "1985-04-12",
      "salutationId": "0x...",
      "salutation": {
        "id": "0x...",
        "salutationKey": "mr",
        "displayName": "Herr",
        "letterName": "Sehr geehrter Herr"
      },
      "createdAt": "2023-06-01 09:15:00",
      "updatedAt": "2024-01-20 14:30:00",
      "newsletter": true,
      "salesChannelId": "0x...",
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    },
    "salesChannelId": "0x...",
    "salesChannel": {
      "id": "0x...",
      "name": "Storefront"
    }
  }
}
```

**Status: 400 Bad Request** (bei ungültigen Parametern)

```json
{
  "status": "error",
  "message": "customerId is required."
}
```

**Status: 400 Bad Request** (bei ungültiger salesChannelId)

Wenn `salesChannelId` angegeben wird, aber kein gültiger Sales Channel existiert:

```json
{
  "status": "error",
  "message": "Invalid salesChannelId. The given sales channel does not exist."
}
```

**Status: 404 Not Found** (wenn Kunde nicht gefunden)

```json
{
  "status": "error",
  "message": "Customer not found."
}
```

### Hinweise

* **Punkte hinzufügen**: Punkte werden über `releasePoints` verarbeitet, was das Profil automatisch aktualisiert und Level-Änderungen berücksichtigt.
* **Nur positive Werte**: Nur positive Werte (größer als 0) sind erlaubt. Negative Werte werden abgelehnt.
* **Profil-Erstellung**: Wenn der Kunde noch kein Profil hat, wird automatisch eines erstellt.
* **Transaktion**: Für jede Punkte-Änderung wird eine Transaktion erstellt, die in der Historie sichtbar ist.
* **salesChannelId**: Ist optional. Wenn angegeben, wird die ID validiert; bei ungültiger ID wird ein Fehler zurückgegeben und keine Transaktion angelegt. Wird `null` übergeben oder der Parameter weggelassen, gilt die Konfiguration für alle Verkaufskanäle. Wird für den übergebenen Sales Channel kein Profil gefunden, fällt die Zuweisung auf ein etwaiges globales Profil (`salesChannelId = null`) zurück.

## Punkte abziehen

Zieht Punkte von einem bestimmten Kunden ab. Nur positive Werte sind erlaubt (werden intern als negative Transaktion gespeichert).

### Endpunkt

```http
POST /api/cogi/loyalty/remove-points
```

### Request Body

| Parameter        | Typ                   | Pflicht | Beschreibung                                                                                                                                                               |
| ---------------- | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`     | string (UUID)         | Ja      | Die ID des Kunden                                                                                                                                                          |
| `points`         | integer               | Ja      | Anzahl der abzuziehenden Punkte (muss positiv sein, größer als 0)                                                                                                          |
| `description`    | string                | Nein    | Beschreibung der Transaktion (Standard: "Manuell abgezogen via API")                                                                                                       |
| `salesChannelId` | string (UUID) \| null | Nein    | ID des Sales Channels. Wenn angegeben, muss die ID einen existierenden Sales Channel bezeichnen. Bei `null` oder Weglassen gilt die Konfiguration für alle Verkaufskanäle. |

### Beispiel-Request

**Punkte abziehen:**

```json
{
  "customerId": "0x...",
  "points": 50,
  "description": "Korrektur wegen fehlerhafter Gutschrift",
  "salesChannelId": "0x..."
}
```

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "message": "Points deducted successfully.",
  "transactionId": "0x...",
  "data": {
    "id": "0x...",
    "customerId": "0x...",
    "points": 450,
    "pointsExpirationDate": "2025-12-31",
    "customFieldRewards": null,
    "setBirthday": true,
    "newsletter": false,
    "active": true,
    "blocked": false,
    "optInConfirmedAt": "2024-01-15 10:30:00",
    "optOutAt": null,
    "birthday": "1985-04-12",
    "lastPointsReceived": "2024-01-20 14:30:00",
    "lastOrderDate": "2024-01-20 09:12:44",
    "createdAt": "2024-01-15 10:30:00",
    "updatedAt": "2024-01-21 11:00:00",
    "level": {
      "id": "0x...",
      "name": "Silver",
      "minPoints": 100,
      "tagId": "0x...",
      "multiplier": 1.5,
      "color": "#FF5733"
    },
    "previousLevel": {
      "id": "0x...",
      "name": "Bronze",
      "minPoints": 0,
      "tagId": "0x...",
      "multiplier": 1.0,
      "color": "#C0C0C0",
      "pointsToFallDown": 350
    },
    "nextLevel": {
      "id": "0x...",
      "name": "Gold",
      "minPoints": 1000,
      "tagId": "0x...",
      "multiplier": 2.0,
      "color": "#FFD700",
      "pointsToReach": 550
    },
    "customer": {
      "id": "0x...",
      "email": "customer@example.com",
      "firstName": "Max",
      "lastName": "Mustermann",
      "customerNumber": "CUST-001",
      "birthday": "1985-04-12",
      "salutationId": "0x...",
      "salutation": {
        "id": "0x...",
        "salutationKey": "mr",
        "displayName": "Herr",
        "letterName": "Sehr geehrter Herr"
      },
      "createdAt": "2023-06-01 09:15:00",
      "updatedAt": "2024-01-20 14:30:00",
      "newsletter": true,
      "salesChannelId": "0x...",
      "salesChannel": {
        "id": "0x...",
        "name": "Storefront"
      }
    },
    "salesChannelId": "0x...",
    "salesChannel": {
      "id": "0x...",
      "name": "Storefront"
    }
  }
}
```

**Status: 400 Bad Request** (bei ungültigen Parametern oder unzureichenden Punkten)

```json
{
  "status": "error",
  "message": "Insufficient points. Customer has 30 points, but 50 points were requested to be deducted."
}
```

**Status: 400 Bad Request** (bei ungültiger salesChannelId)

Wenn `salesChannelId` angegeben wird, aber kein gültiger Sales Channel existiert:

```json
{
  "status": "error",
  "message": "Invalid salesChannelId. The given sales channel does not exist."
}
```

**Status: 404 Not Found** (wenn Kunde oder Profil nicht gefunden)

```json
{
  "status": "error",
  "message": "Customer profile not found. Cannot deduct points from non-existent profile."
}
```

### Hinweise

* **Punkte abziehen**: Punkte werden direkt vom Profil abgezogen. Das Level wird automatisch neu berechnet.
* **Nur positive Werte**: Nur positive Werte (größer als 0) sind erlaubt. Der Wert wird intern als negative Transaktion gespeichert.
* **Punkte-Prüfung**: Der Endpunkt prüft, ob der Kunde genügend Punkte hat. Bei unzureichenden Punkten wird ein Fehler zurückgegeben.
* **Profil erforderlich**: Der Kunde muss bereits ein Profil haben. Es wird kein Profil erstellt, wenn keines existiert. Gibt es für den angeforderten Sales Channel kein Profil, fällt die Logik auf ein globales Profil (`salesChannelId = null`) zurück, falls vorhanden.
* **Transaktion**: Für jeden Punkte-Abzug wird eine Transaktion mit negativem Wert erstellt, die in der Historie sichtbar ist.
* **salesChannelId**: Ist optional. Wenn angegeben, wird die ID validiert; bei ungültiger ID wird ein Fehler zurückgegeben und keine Transaktion angelegt. Bei Weglassen wird zuerst der Standard-Kanal des Kunden beachtet und ansonsten auf das globale Profil zurückgegriffen.

## Datenstrukturen

### Profile-Objekt

Das Profil-Objekt enthält folgende Felder:

| Feld                      | Typ                       | Beschreibung                                                                                                             |
| ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id`                      | string (UUID)             | Eindeutige ID des Profils                                                                                                |
| `customerId`              | string (UUID)             | ID des zugehörigen Kunden                                                                                                |
| `points`                  | integer                   | Aktuelle Anzahl der Punkte                                                                                               |
| `pointsExpirationDate`    | string (date) \| null     | Ablaufdatum der Punkte (falls vorhanden)                                                                                 |
| `customFieldRewards`      | array \| null             | Custom Field Rewards                                                                                                     |
| `setBirthday`             | boolean                   | Ob Geburtstag gesetzt ist                                                                                                |
| `newsletter`              | boolean                   | Newsletter-Abonnement Status                                                                                             |
| `active`                  | boolean                   | Ob das Loyalty-Profil aktiv ist                                                                                          |
| `blocked`                 | boolean                   | Ob das Loyalty-Profil blockiert ist                                                                                      |
| `optInConfirmedAt`        | string (datetime) \| null | Bestätigungsdatum des Opt-Ins                                                                                            |
| `optOutAt`                | string (datetime) \| null | Zeitpunkt der Abmeldung vom Loyalty-Programm                                                                             |
| `birthday`                | string (date) \| null     | Geburtsdatum des Kunden (`YYYY-MM-DD`)                                                                                   |
| `lastPointsReceived`      | string (datetime) \| null | Datum und Uhrzeit, wann der Kunde zuletzt Punkte erhalten hat                                                            |
| `lastOrderDate`           | string (datetime) \| null | Datum und Uhrzeit der letzten Bestellung des Kunden                                                                      |
| `createdAt`               | string (datetime)         | Erstellungsdatum des Profils                                                                                             |
| `updatedAt`               | string (datetime)         | Letztes Aktualisierungsdatum                                                                                             |
| `salesChannelId`          | string (UUID) \| null     | ID des Sales Channels, auf den das **Loyalty-Profil** eingeschränkt ist (`null` = globales Profil, gilt für alle Kanäle) |
| `customer.salesChannelId` | string (UUID)             | ID des Sales Channels, in dem der **Kunde registriert** ist                                                              |
| `customer.salesChannel`   | object \| null            | Der Sales Channel des Kunden mit `id` und `name`                                                                         |
| `customer.newsletter`     | boolean                   | Ob der Kunde den **Newsletter abonniert** hat                                                                            |
| `customer.salutationId`   | string (UUID) \| null     | ID der **Anrede** des Kunden                                                                                             |
| `customer.salutation`     | object \| null            | Die Anrede des Kunden mit `id`, `salutationKey`, `displayName` und `letterName`                                          |
| `customer.createdAt`      | string (datetime)         | Anlagedatum des **Kundenkontos**                                                                                         |
| `customer.updatedAt`      | string (datetime) \| null | Letzte Änderung des **Kundenkontos**                                                                                     |

{% hint style="info" %}
**Hinweis zu den beiden `newsletter`-Feldern:** `customer.newsletter` gibt die tatsächliche Newsletter-Anmeldung des Kunden wieder — `true`, sobald zu seiner E-Mail-Adresse ein Newsletter-Empfänger mit dem Status `direct` oder `optIn` existiert (kanalübergreifend). Das ist dieselbe Definition, die Shopware selbst für die Newsletter-Zuordnung am Kunden verwendet: `direct` entsteht in Shops ohne Double-Opt-in, `optIn` nach bestätigtem Double-Opt-in. Die Status `notSet` (Bestätigung ausstehend) und `optOut` (abgemeldet) ergeben `false`.

Das gleichnamige Feld `newsletter` auf oberster Ebene gehört dagegen zum Treueprofil und hält fest, ob die Newsletter-Punkte bereits gutgeschrieben wurden. Ein Kunde, der sich vom Newsletter abmeldet, behält damit `newsletter: true` auf Profilebene, bekommt aber `customer.newsletter: false`.
{% endhint %}

{% hint style="info" %}
**Hinweis zu den beiden Sales Channels:** `salesChannelId` auf oberster Ebene beschreibt den Geltungsbereich des Loyalty-Profils und ist bei globalen Profilen `null`. `customer.salesChannelId` beschreibt dagegen den Verkaufskanal des Kundenkontos und ist immer gesetzt. Beide können voneinander abweichen — etwa wenn ein Kunde aus Kanal A ein globales Profil besitzt.
{% endhint %}

{% hint style="info" %}
**Hinweis:** `lastPointsReceived` ist `null`, wenn der Kunde noch nie Punkte erhalten hat oder wenn keine Transaktion mit positivem Wert gefunden werden kann.
{% endhint %}

{% hint style="info" %}
**Hinweis zu `optOutAt`:** Das Feld wird ausschließlich beim expliziten Opt-out des Kunden im Storefront gesetzt (Loyalty-Checkbox im Kundenkonto), und zwar nur beim tatsächlichen Wechsel von aktiv zu inaktiv — ein wiederholtes Absenden verschiebt das Datum nicht. Deaktiviert oder sperrt ein Shop-Mitarbeiter ein Profil in der Administration, bleibt `optOutAt` unverändert.

Geleert wird das Datum dagegen bei **jeder** Reaktivierung eines Profils, egal über welchen Weg (Storefront, Administration, Admin-API, CSV-/CLI-Import). Ein gesetztes `optOutAt` bedeutet damit zuverlässig „seit diesem Zeitpunkt abgemeldet" und bleibt nicht als Altlast an einem wieder aktiven Profil hängen. Profile, die vor der Einführung des Feldes abgemeldet wurden, haben `null`.
{% endhint %}

{% hint style="info" %}
**Hinweis zur Anrede:** `customer.salutation` ist ein übersetztes Objekt – `displayName` und `letterName` werden in der Sprache der Anfrage ausgegeben (Header `sw-language-id`), `salutationKey` (z. B. `mr`, `mrs`, `not_specified`) ist sprachunabhängig und eignet sich für Auswertungen. Kunden ohne hinterlegte Anrede liefern `salutationId: null` und `salutation: null`.
{% endhint %}

{% hint style="info" %}
**Hinweis zu `customer.createdAt` / `customer.updatedAt`:** Diese Felder beziehen sich auf das **Kundenkonto**, die gleichnamigen Felder auf oberster Ebene dagegen auf das **Treueprofil**. Beide können deutlich voneinander abweichen, etwa wenn ein Kunde erst lange nach seiner Registrierung am Treueprogramm teilnimmt. `customer.updatedAt` ist `null`, solange das Kundenkonto seit der Anlage nicht geändert wurde.
{% endhint %}

{% hint style="info" %}
**Hinweis zu `birthday`:** Das Geburtsdatum stammt vom Kundenstammsatz und ist zusätzlich im verschachtelten `customer`-Objekt enthalten. Nicht zu verwechseln mit dem Boolean `setBirthday`, das nur angibt, ob der Kunde bereits Punkte für das Hinterlegen seines Geburtstags erhalten hat.
{% endhint %}

{% hint style="info" %}
**Hinweis zu `lastOrderDate`:** Berücksichtigt werden alle Bestellungen des Kunden unabhängig vom Bestellstatus (auch stornierte). Ist das Profil einem Sales Channel zugeordnet, werden nur Bestellungen dieses Kanals berücksichtigt; bei einem globalen Profil (`salesChannelId = null`) zählen alle Kanäle. Das Feld ist `null`, wenn der Kunde im betrachteten Kanal noch keine Bestellung hat.
{% endhint %}

### Level-Objekt

```json
{
  "id": "string (UUID)",
  "name": "string",
  "minPoints": "integer",
  "tagId": "string (UUID)",
  "multiplier": "float",
  "color": "string (hex color)"
}
```

### Previous Level-Objekt

Enthält alle Felder des Level-Objekts plus:

```json
{
  "pointsToFallDown": "integer"  // Anzahl Punkte, die noch verloren werden können, bevor Abstieg erfolgt
}
```

### Next Level-Objekt

Enthält alle Felder des Level-Objekts plus:

```json
{
  "pointsToReach": "integer"  // Anzahl Punkte, die noch benötigt werden, um dieses Level zu erreichen
}
```

### Customer-Objekt

```json
{
  "id": "string (UUID)",
  "email": "string",
  "firstName": "string",
  "lastName": "string",
  "customerNumber": "string"
}
```

### Transaction-Objekt

```json
{
  "id": "string (UUID)",
  "customerId": "string (UUID)",
  "orderId": "string (UUID) | null",
  "salesChannelId": "string (UUID) | null",
  "value": "integer",  // Positiv für Gutschriften, negativ für Abzüge
  "description": "string | null",
  "type": "string",  // earn, spend, refund_spend, refund_earn, expire, manual_add, manual_deduct, api_add, api_deduct, interval_deduction
  "received": "boolean",
  "sourceTransactionId": "string (UUID) | null",
  "expirationDate": "string (datetime) | null",
  "createdAt": "string (datetime)",
  "updatedAt": "string (datetime)"
}
```

### Order-Objekt (in Transaction)

```json
{
  "id": "string (UUID)",
  "orderNumber": "string",
  "amountTotal": "float",
  "currency": "string (ISO code)"
}
```

## Fehlerbehandlung

Alle Endpunkte können folgende Fehlerantworten zurückgeben:

### 400 Bad Request

```json
{
  "status": "error",
  "message": "Fehlerbeschreibung"
}
```

### 404 Not Found

```json
{
  "status": "error",
  "message": "Resource not found."
}
```

### 500 Internal Server Error

```json
{
  "status": "error",
  "message": "Error description: [Detaillierte Fehlerbeschreibung]"
}
```

## Hinweise

1. **Pagination**: Verwenden Sie `limit` und `offset` für große Datensätze. Die Standard-Limit beträgt 20 Einträge.
2. **Sortierung**: Standardmäßig werden Ergebnisse nach `createdAt` (Transaktionen) bzw. `points` (Profile) in absteigender Reihenfolge sortiert.
3. **Filter-Kombinationen**: Mehrere Filter können kombiniert werden, um präzise Ergebnisse zu erhalten.
4. **Assoziationen**: Order-, Customer- und SalesChannel-Informationen werden nur zurückgegeben, wenn sie verfügbar sind. Prüfen Sie auf `null`-Werte.
5. **Punkte-Werte**: In Transaktionen sind positive Werte Gutschriften und negative Werte Abzüge.
6. **UUID-Format**: Alle IDs sind UUIDs im Binärformat (0x...). Stellen Sie sicher, dass Ihre Client-Implementierung diese korrekt verarbeitet.

## Beispiele für häufige Use Cases

### Alle Profile eines bestimmten Levels abrufen

```http
GET /api/cogi/loyalty/customer-profiles?levelId=0x...&limit=100
```

### Top-Kunden nach Punkten abrufen

```http
GET /api/cogi/loyalty/customer-profiles?sortBy=points&sortDirection=DESC&limit=10
```

### Alle Transaktionen eines Kunden für eine Bestellung

```http
GET /api/cogi/loyalty/customer-transactions/0x...?orderId=0x...
```

### Nur aktive (nicht abgelaufene) Transaktionen

```http
GET /api/cogi/loyalty/customer-transactions/0x...?expired=false
```

### Alle Transaktionen mit Gutschriften (positive Werte)

```http
GET /api/cogi/loyalty/customer-transactions/0x...?minValue=1
```

### Punkte für einen Kunden hinzufügen

```http
POST /api/cogi/loyalty/add-points
Content-Type: application/json

{
  "customerId": "0x...",
  "points": 100,
  "description": "Bonus für Kundenfeedback"
}
```

### Punkte von einem Kunden abziehen

```http
POST /api/cogi/loyalty/remove-points
Content-Type: application/json

{
  "customerId": "0x...",
  "points": 50,
  "description": "Korrektur wegen fehlerhafter Gutschrift"
}
```

## Verfügbare Aktionen abrufen

Ruft alle Aktionen ab, die der Kunde noch ausführen kann, um weitere Punkte zu erhalten.

### Endpunkt

```http
GET /api/cogi/loyalty/available-actions/{customerId}
```

### Parameter

| Parameter    | Typ           | Pflicht | Beschreibung      |
| ------------ | ------------- | ------- | ----------------- |
| `customerId` | string (path) | Ja      | Die ID des Kunden |

### Query-Parameter

| Parameter        | Typ           | Pflicht | Beschreibung                                                          |
| ---------------- | ------------- | ------- | --------------------------------------------------------------------- |
| `salesChannelId` | string (UUID) | Nein    | ID des Sales Channels (falls nicht vom Kunden abgeleitet werden kann) |

### Response

**Status: 200 OK**

```json
{
  "status": "success",
  "data": {
    "availableActions": [
      {
        "type": "newsletter",
        "name": "Newsletter",
        "points": 50,
        "completed": false,
        "description": "Newsletter abonnieren"
      },
      {
        "type": "birthday",
        "name": "Geburtstag",
        "points": 100,
        "completed": false,
        "description": "Geburtstag im Profil speichern"
      },
      {
        "type": "customField",
        "name": "customFieldName",
        "points": 25,
        "completed": true,
        "description": "Profilinformationen vervollständigen (bereits ausgefüllt, wartet auf Belohnung)",
        "fieldFilled": true
      }
    ],
    "totalAvailablePoints": 175,
    "count": 3
  }
}
```

**Status: 404 Not Found** (wenn Kunde oder Profil nicht gefunden)

```json
{
  "status": "error",
  "message": "Customer not found."
}
```

### Verfügbare Aktionstypen

| Typ           | Beschreibung                     |
| ------------- | -------------------------------- |
| `newsletter`  | Newsletter-Abonnement            |
| `birthday`    | Geburtstag im Profil setzen      |
| `customField` | Custom Field im Profil ausfüllen |

### Response-Felder

| Feld          | Typ     | Beschreibung                                                     |
| ------------- | ------- | ---------------------------------------------------------------- |
| `type`        | string  | Typ der Aktion (newsletter, birthday, customField)               |
| `name`        | string  | Name der Aktion                                                  |
| `points`      | integer | Anzahl der Punkte, die für diese Aktion vergeben werden          |
| `completed`   | boolean | Ob die Aktion bereits ausgeführt wurde (aber noch nicht belohnt) |
| `description` | string  | Beschreibung der Aktion                                          |
| `fieldFilled` | boolean | (Nur bei customField) Ob das Feld bereits ausgefüllt ist         |

### Hinweise

* **Newsletter**: Wird nur angezeigt, wenn der Kunde noch kein Newsletter-Abonnement hat und noch keine Newsletter-Punkte erhalten hat.
* **Geburtstag**: Wird nur angezeigt, wenn der Geburtstag noch nicht gesetzt wurde und noch keine Geburtstags-Punkte erhalten wurden.
* **Custom Fields**: Werden nur angezeigt, wenn die Felder konfiguriert sind und noch nicht belohnt wurden. Die Aktion wird auch angezeigt, wenn das Feld bereits ausgefüllt ist, aber noch keine Belohnung erhalten hat.

## Transaktionstypen – Referenz

### Verfügbare Transaktionstypen

Das Loyalty Plugin verwendet die folgenden Transaktionstypen:

| Typ                  | Beschreibung                                             | Received | Value   |
| -------------------- | -------------------------------------------------------- | -------- | ------- |
| `earn`               | Punkte durch Bestellung verdient                         | true     | positiv |
| `spend`              | Punkte im Checkout eingelöst                             | false    | negativ |
| `refund_spend`       | Rückerstattung eingelöster Punkte (z.B. bei Stornierung) | true     | positiv |
| `refund_earn`        | Rücknahme verdienter Punkte (z.B. bei Stornierung)       | false    | negativ |
| `expire`             | Punkte sind abgelaufen                                   | false    | negativ |
| `manual_add`         | Manuelles Hinzufügen von Punkten (Admin)                 | true     | positiv |
| `manual_deduct`      | Manuelles Abziehen von Punkten (Admin)                   | false    | negativ |
| `api_add`            | Punkte via API hinzugefügt                               | true     | positiv |
| `api_deduct`         | Punkte via API abgezogen                                 | false    | negativ |
| `interval_deduction` | Periodischer Punkteabzug (z.B. monatlich)                | false    | negativ |

### Beispiele für Typ-Filter

**Nur verdiente Punkte (earn):**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?type=earn
```

**Nur manuelle Hinzufügungen:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?type=manual_add
```

**Nur eingelöste Punkte:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?type=spend
```

**Nur abgelaufene Punkte:**

```http
GET /api/cogi/loyalty/customer-transactions/0x...?type=expire
```

### Hinweise

* **Received**: Gibt an, ob die Transaktion für den Kunden einen Punktezuwachs darstellt (true) oder einen Punkteabzug (false)
* **Value**: Positive Werte sind Gutschriften, negative Werte sind Abzüge
* **FIFO-Prinzip**: Bei Punkteabzügen (spend, manual\_deduct, api\_deduct, expire, interval\_deduction) werden Punkte nach dem FIFO-Prinzip (First-In-First-Out) abgezogen
* **Source Transaction**: Bei Punkteabzügen wird im Feld `sourceTransactionId` die ID der ursprünglichen Earn-Transaktion gespeichert, von der die Punkte abgezogen wurden


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.codegiganten.de/plugin-documentation/apps/enterprise-loyalty-bonus-programm/api-zugriff.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
