Zum Inhalt
v26.3

Disco-API (Cashless-Club)

Das Disco-Modul bildet einen komplett unbar geführten Club-/Diskothekenbetrieb ab: Jeder Gast bekommt beim Einlass eine Karte (Chip/RFID/NFC), die während des ganzen Aufenthalts als Konto dient. Alles — Eintritt, Getränke, Garderobe, Tischbestellungen — wird auf die Karte gebucht und erst beim Verlassen (oder per Prepaid-Aufladung) abgerechnet. Die API deckt den kompletten Lebenszyklus einer Karte ab: einlassen → buchen → aufladen → abrechnen → verlassen.

Alle Endpoints liegen unter dem gemeinsamen Präfix api/v1/disco (drei Controller teilen sich diesen Prefix: Gäste/Karten, Statistiken, Konfiguration — plus die Drehkreuz-Hardware-API unter api/v1/disco/turnstile). Wie bei allen Bereichen: JWT-Bearer, Response-Envelope { "success": true, "data": { ... } }, camelCase-Felder, opake IDs.

Erfordert das Modul Disco

Erfordert das Modul Disco; ohne aktive Modul-Lizenz liefern die Endpoints HTTP 403 (errorCode: "MODULE_NOT_LICENSED").

Karten-ID vs. interne ID

Jeder Datensatz hat eine interne id (Datenbank-Schlüssel) und eine guestId — das ist die Kartennummer, mit der der Chip physisch beschriftet ist. Alle {cardId}-Routen erwarten die guestId, nicht die interne id.

Gäste & Karten

DiscoController — Basis api/v1/disco:

Methode Endpoint Zweck
POST /enter Gast am Drehkreuz einlassen (Karten-Scan Eingang)
POST /leave Gast auschecken mit Zahlung (Scan Ausgang)
POST /leave-combined Mehrere Karten („Pärchen") gemeinsam auf einem Beleg abrechnen & schließen
POST /book Artikel auf Karte buchen (Bar/Theke)
POST /book/batch Mehrere Artikel auf einmal auf Karte buchen
POST /book/{guestId}/{bonIndex}/void Gebuchte Position stornieren
POST /table/book-on-card Tischbestellung auf Karte buchen (löst Tisch-Zahlung ab)
POST /wardrobe/return/{guestId}/{bonIndex} Garderoben-Rückgabe (entfernt #Nr., Betrag bleibt)
GET /guest/{cardId} Gast/Karte inkl. aktuellem Umsatz (nur offene Karten)
GET /guest/{cardId}/status Kartenstatus, auch gesperrt/storniert/abgerechnet
GET /guest/{cardId}/checkout Abrechnungsansicht: offener Betrag, Prepaid-Deckung, darf-gehen
GET /guests Alle aktuell anwesenden (eingecheckten) Gäste
POST /guest/{cardId}/topup Prepaid-Guthaben aufladen
POST /guest/{cardId}/transfer Karte/Guthaben auf andere Karte übertragen (Kartentausch)
POST /guest/{cardId}/lock Karte sperren (keine Buchungen mehr möglich)
POST /guest/{cardId}/unlock Karte entsperren
POST /guest/{cardId}/storno Ganze Karte stornieren + als Nichtzahler markieren
POST /guest/{cardId}/collect-storno Storno-Karte an der Kasse nachkassieren
POST /guest/{cardId}/image Karten-Foto hinterlegen
POST /terminal/pay Selbstbedienungs-EC-Zahlung am Infoterminal auslösen
POST /personal/{cardId}/close Eine Personalkarte abrechnen (Eigenverbrauch)
POST /personal/close-all Alle offenen Personalkarten als Eigenverbrauch abrechnen
POST /close-all Alle offenen Karten schließen (Betriebsende/Tagesabschluss)

Auf Karte buchen

POST /api/v1/disco/book bucht einen Artikel auf eine offene Karte (Bar/Theke-Verkauf ohne Zahlung — die Zahlung erfolgt erst bei leave/Checkout).

POST /api/v1/disco/book
Authorization: Bearer eyJ...
Content-Type: application/json

{
  "guestId": "crd_10042",
  "articleId": "art_cola",
  "articleName": "Cola 0,3l",
  "count": 2,
  "price": 3.50,
  "workplaceId": "wp_bar1",
  "workplaceName": "Bar 1"
}

Antwort (200 OK, DiscoGuestResponse):

{
  "success": true,
  "data": {
    "id": "dgu_9f8e7d",
    "guestId": "crd_10042",
    "gender": 1,
    "enterDate": "2026-07-12T22:05:00Z",
    "amount": 7.00,
    "payedAmount": 0,
    "uploadAccount": 20.00,
    "iou": 0,
    "isOpen": true,
    "isLocked": false,
    "openBons": [
      { "articleId": "art_cola", "articleName": "Cola 0,3l", "price": 3.50, "count": 2, "total": 7.00,
        "workplaceId": "wp_bar1", "workplaceName": "Bar 1", "bookDate": "2026-07-12T23:10:00Z", "isVoided": false }
    ]
  }
}

Kartenstatus

GET /api/v1/disco/guest/{cardId}/status liefert den Status unabhängig davon, ob die Karte noch offen ist — anders als GET /guest/{cardId}, das nur offene Karten findet. Damit erkennt Bar/ Garderobe/Kasse auch gesperrte oder stornierte Karten.

GET /api/v1/disco/guest/crd_10042/status
Authorization: Bearer eyJ...
{
  "success": true,
  "data": {
    "found": true,
    "isOpen": true,
    "isLocked": false,
    "stornoed": false,
    "guest": { "id": "dgu_9f8e7d", "guestId": "crd_10042", "amount": 7.00, "uploadAccount": 20.00 }
  }
}

Aktueller Umsatz

GET /api/v1/disco/guest/{cardId} liefert dieselbe DiscoGuestResponse wie oben, inkl. der laufenden offenen Buchungen (openBons) und des Prepaid-Guthabens (uploadAccount). Antwort 404 GUEST_NOT_FOUND, wenn die Karte nicht (mehr) offen ist.

Abrechnung/Checkout

GET /api/v1/disco/guest/{cardId}/checkout verändert nichts — reine Vorschau, was die Karte noch schuldet, was das Prepaid-Guthaben deckt und ob sie automatisch raus darf (Selbst-Checkout, mobile Ausgangskasse, Drehkreuz).

{
  "success": true,
  "data": {
    "cardId": "crd_10042",
    "found": true,
    "isOpen": true,
    "isLocked": false,
    "consumption": 7.00,
    "netOwed": 7.00,
    "prepaid": 20.00,
    "amountToPay": 0,
    "refund": 13.00,
    "canAutoSettle": true,
    "status": "ok",
    "message": "Karte kann raus — Restguthaben 13.00 €"
  }
}

status ist eine von ok | pay | locked | closed | notfound.

Tisch auf Karte

POST /api/v1/disco/table/book-on-card verlagert die offenen Positionen eines Tisches auf eine Disco-Karte (Disco-Tischmodus) — ersetzt die Bar-/EC-Zahlung am Tisch, die Karten-Limits gelten weiter, der Tisch wird frei.

{ "tableId": "tbl_12", "cardId": "crd_10042", "openBonIds": null }

openBonIds: null überträgt alle offenen Positionen; eine Liste überträgt nur eine „Sofort"-Runde.

Aufladen

POST /api/v1/disco/guest/{cardId}/topup lädt das Karten-Prepaid-Guthaben (uploadAccount) auf — der Betrag ist frei wählbar, ein Restguthaben wird beim Checkout zurückerstattet.

{ "amount": 20.00, "paymentMethod": "Bar" }

Antwort ist wieder die aktualisierte DiscoGuestResponse (uploadAccount erhöht).

Verwechslungsgefahr: Disco-Karten-Guthaben vs. Kundenkonto-Guthaben

Das hier ist das Karten-Prepaid eines einzelnen Disco-Aufenthalts (verfällt/erstattet sich beim Verlassen). Ein dauerhaftes Kundenkonto-Guthaben ist ein eigenes Konzept — siehe Kundenkonto: Guthaben & Punkte.

Statistiken

DiscoStatsController — Basis api/v1/disco:

Methode Endpoint Zweck
GET /stats Live-Kennzahlen (Umsatz, anwesende Gäste, Auslastung nach Gruppe/Arbeitsplatz/Personal/Stunde)
GET /search Karten/Gäste suchen (Kartennummer, Name, Zeitraum, Geschlecht, Storno-Filter, Paging)
GET /personal/{ownerId}/activity Aktivität einer Personalkarte (Verzehr, Storno, Bruch)
GET /daylog/{date} Tagesprotokoll für ein Datum

Beispiel: GET /api/v1/disco/stats

{
  "success": true,
  "data": {
    "totalGuests": 214,
    "currentGuests": 87,
    "totalMale": 96,
    "totalFemale": 118,
    "totalRevenue": 4211.50,
    "averageSpend": 19.68,
    "byGroup": [ { "groupId": "eg_std", "groupName": "Standard", "count": 180, "revenue": 3500.00 } ],
    "byWorkplace": [ { "workplaceId": "wp_bar1", "workplaceName": "Bar 1", "count": 400, "revenue": 2200.00 } ],
    "byStaff": [ { "personalId": "per_1", "personalName": "Mo", "count": 120, "revenue": 900.00 } ],
    "byHour": [ { "hour": 23, "count": 60, "revenue": 800.00 } ]
  }
}

GET /search unterstützt u. a. cardId, name, dateFrom/dateTo, gender, timeFrom/timeTo, workplaceId, enterGroupId, wardrobe, stornoed, offset/limit, excludeImages.

Konfiguration

DiscoConfigController — Basis api/v1/disco, klassisches CRUD für Eintritts-/Preisgruppen („Entergroups" — legen z. B. Eintrittspreis, Kartenlimit, Freigetränke, Mindestverzehr fest):

Methode Endpoint Zweck
GET /entergroups Alle Eintrittsgruppen
GET /entergroups/{id} Einzelne Eintrittsgruppe
POST /entergroups Eintrittsgruppe anlegen
PUT /entergroups/{id} Eintrittsgruppe aktualisieren
DELETE /entergroups/{id} Eintrittsgruppe löschen

Drehkreuz-Hardware

DiscoTurnstileControllerapi/v1/disco/turnstile/{key}/{cardId} (GET/POST).

Kein JWT — Authentifizierung per API-Key in der URL ({key}), damit ein einfacher Drehkreuz-Controller die Route ohne Login-Flow ansprechen kann. 200 = Karte darf raus, jeder andere Code trägt einen Grund (gesperrt/offen/bereits abgerechnet/unbekannt/falscher Key).

→ Details, Einrichtung und Statuscode-Tabelle: Disco-Drehkreuz.

Ende-zu-Ende-Flow

Ein typischer Gastdurchlauf, alle Schritte gegen crd_10042:

TOKEN="eyJ..."
API="https://ihre-instanz.dikas.de/api/v1"

# 1. Einlass am Drehkreuz (Kartenscan Eingang)
curl -X POST "$API/disco/enter" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "guestId": "crd_10042", "gender": 1, "enterGroupId": "eg_std" }'

# 2. Status prüfen (z. B. Wiedereinlass nach kurzem Verlassen)
curl "$API/disco/guest/crd_10042/status" -H "Authorization: Bearer $TOKEN"

# 3. Prepaid aufladen
curl -X POST "$API/disco/guest/crd_10042/topup" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "amount": 20.00, "paymentMethod": "Bar" }'

# 4. An der Bar buchen (einzeln oder als Batch)
curl -X POST "$API/disco/book" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "guestId": "crd_10042", "articleId": "art_cola", "articleName": "Cola 0,3l", "count": 2, "price": 3.50 }'

# 5. Laufenden Umsatz abfragen
curl "$API/disco/guest/crd_10042" -H "Authorization: Bearer $TOKEN"

# 6. Tischbestellung auf die Karte buchen
curl -X POST "$API/disco/table/book-on-card" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "tableId": "tbl_12", "cardId": "crd_10042" }'

# 7. Vor dem Ausgang: Checkout-Vorschau
curl "$API/disco/guest/crd_10042/checkout" -H "Authorization: Bearer $TOKEN"

# 8. Verlassen mit Zahlung (falls noch etwas offen ist)
curl -X POST "$API/disco/leave" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "cardId": "crd_10042", "payments": [ { "method": "Cash", "amount": 0 } ] }'

Störfall — Karte kann am Ausgang nicht bezahlen (Nichtzahler):

# Karte stornieren: als Nichtzahler markieren, Verzehr als Schwund verbuchen
curl -X POST "$API/disco/guest/crd_10042/storno" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "reason": "Karte verloren" }'

# Später an der Kasse nachkassiert
curl -X POST "$API/disco/guest/crd_10042/collect-storno" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "payments": [ { "method": "Cash", "amount": 7.00 } ] }'