Vai al contenuto
v26.3

API Disco (Club Cashless)

Il modulo Disco rappresenta un'attività di club/discoteca gestita in modo completamente senza contanti: ogni ospite riceve all'ingresso una carta (chip/RFID/NFC) che funge da conto per l'intera permanenza. Tutto — ingresso, bevande, guardaroba, ordinazioni al tavolo — viene addebitato sulla carta e regolato solo all'uscita (oppure tramite ricarica prepagata). L'API copre l'intero ciclo di vita di una carta: ingresso → addebito → ricarica → regolamento → uscita.

Tutti gli endpoint si trovano sotto il prefisso comune api/v1/disco (tre controller condividono questo prefisso: ospiti/carte, statistiche, configurazione — più l'API hardware del tornello sotto api/v1/disco/turnstile). Come in tutti gli ambiti: JWT-Bearer, envelope di risposta { "success": true, "data": { ... } }, campi in camelCase, ID opachi.

Richiede il modulo Disco

Richiede il modulo Disco; senza licenza attiva del modulo, gli endpoint restituiscono HTTP 403 (errorCode: "MODULE_NOT_LICENSED").

ID carta vs. ID interno

Ogni record ha un id interno (chiave del database) e un guestId — è questo il numero della carta con cui il chip è fisicamente contrassegnato. Tutte le rotte {cardId} si aspettano il guestId, non l'id interno.

Ospiti & carte

DiscoController — base api/v1/disco:

Metodo Endpoint Scopo
POST /enter Far entrare l'ospite al tornello (scansione carta all'ingresso)
POST /leave Far uscire l'ospite con pagamento (scansione all'uscita)
POST /leave-combined Regolare e chiudere insieme più carte („coppie") su un unico scontrino
POST /book Addebitare un articolo sulla carta (bar/banco)
POST /book/batch Addebitare più articoli in una volta sulla carta
POST /book/{guestId}/{bonIndex}/void Stornare una posizione addebitata
POST /table/book-on-card Addebitare un'ordinazione al tavolo sulla carta (sostituisce il pagamento al tavolo)
POST /wardrobe/return/{guestId}/{bonIndex} Restituzione al guardaroba (rimuove il n°, l'importo resta)
GET /guest/{cardId} Ospite/carta incl. consumo attuale (solo carte aperte)
GET /guest/{cardId}/status Stato della carta, anche se bloccata/stornata/regolata
GET /guest/{cardId}/checkout Vista di regolamento: importo aperto, copertura prepagata, può uscire
GET /guests Tutti gli ospiti attualmente presenti (con check-in)
POST /guest/{cardId}/topup Ricaricare il credito prepagato
POST /guest/{cardId}/transfer Trasferire carta/credito su un'altra carta (cambio carta)
POST /guest/{cardId}/lock Bloccare la carta (non sono più possibili addebiti)
POST /guest/{cardId}/unlock Sbloccare la carta
POST /guest/{cardId}/storno Stornare l'intera carta e contrassegnarla come moroso
POST /guest/{cardId}/collect-storno Incassare successivamente alla cassa una carta stornata
POST /guest/{cardId}/image Salvare una foto della carta
POST /terminal/pay Avviare un pagamento EC self-service al terminale informativo
POST /personal/{cardId}/close Regolare una carta del personale (consumo proprio)
POST /personal/close-all Regolare tutte le carte del personale aperte come consumo proprio
POST /close-all Chiudere tutte le carte aperte (fine attività/chiusura giornaliera)

Addebitare sulla carta

POST /api/v1/disco/book addebita un articolo su una carta aperta (vendita al bar/banco senza pagamento — il pagamento avviene solo al 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"
}

Risposta (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 }
    ]
  }
}

Stato della carta

GET /api/v1/disco/guest/{cardId}/status restituisce lo stato indipendentemente dal fatto che la carta sia ancora aperta — a differenza di GET /guest/{cardId}, che trova solo le carte aperte. Così bar/guardaroba/cassa riconoscono anche le carte bloccate o stornate.

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 }
  }
}

Consumo attuale

GET /api/v1/disco/guest/{cardId} restituisce la stessa DiscoGuestResponse di cui sopra, incl. gli addebiti aperti in corso (openBons) e il credito prepagato (uploadAccount). Risposta 404 GUEST_NOT_FOUND se la carta non è (più) aperta.

Regolamento/checkout

GET /api/v1/disco/guest/{cardId}/checkout non modifica nulla — è una semplice anteprima di quanto la carta debba ancora, di quanto sia coperto dal credito prepagato e se possa uscire automaticamente (self-checkout, cassa d'uscita mobile, tornello).

{
  "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 è uno tra ok | pay | locked | closed | notfound.

Tavolo su carta

POST /api/v1/disco/table/book-on-card sposta le posizioni aperte di un tavolo su una carta Disco (modalità tavolo Disco) — sostituisce il pagamento in contanti/EC al tavolo, i limiti della carta restano validi, il tavolo si libera.

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

openBonIds: null trasferisce tutte le posizioni aperte; un elenco trasferisce solo un giro „immediato".

Ricaricare

POST /api/v1/disco/guest/{cardId}/topup ricarica il credito prepagato della carta (uploadAccount) — l'importo è liberamente scelto, un credito residuo viene rimborsato al checkout.

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

La risposta è di nuovo la DiscoGuestResponse aggiornata (uploadAccount aumentato).

Rischio di confusione: credito della carta Disco vs. credito del conto cliente

Questo è il prepagato della carta di una singola permanenza in discoteca (decade/viene rimborsato all'uscita). Un credito permanente del conto cliente è un concetto a sé — vedi Conto cliente: credito e punti fedeltà.

Statistiche

DiscoStatsController — base api/v1/disco:

Metodo Endpoint Scopo
GET /stats KPI in tempo reale (fatturato, ospiti presenti, affluenza per gruppo/postazione/personale/ora)
GET /search Cercare carte/ospiti (numero carta, nome, periodo, genere, filtro storni, paginazione)
GET /personal/{ownerId}/activity Attività di una carta del personale (consumo, storno, rotture)
GET /daylog/{date} Registro giornaliero per una data

Esempio: 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 supporta tra l'altro cardId, name, dateFrom/dateTo, gender, timeFrom/timeTo, workplaceId, enterGroupId, wardrobe, stornoed, offset/limit, excludeImages.

Configurazione

DiscoConfigController — base api/v1/disco, classico CRUD per i gruppi di ingresso/prezzo („Entergroups" — stabiliscono ad es. il prezzo d'ingresso, il limite della carta, le bevande omaggio, il consumo minimo):

Metodo Endpoint Scopo
GET /entergroups Tutti i gruppi di ingresso
GET /entergroups/{id} Singolo gruppo di ingresso
POST /entergroups Creare un gruppo di ingresso
PUT /entergroups/{id} Aggiornare un gruppo di ingresso
DELETE /entergroups/{id} Eliminare un gruppo di ingresso

Hardware del tornello

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

Nessun JWT — autenticazione tramite API key nell'URL ({key}), in modo che un semplice controller del tornello possa richiamare la rotta senza un flusso di login. 200 = la carta può uscire, ogni altro codice porta con sé un motivo (bloccata/aperta/già regolata/sconosciuta/key errata).

→ Dettagli, configurazione e tabella dei codici di stato: Tornello Disco.

Flusso end-to-end

Un tipico percorso dell'ospite, tutti i passaggi contro 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 } ] }'

Caso critico — la carta non può pagare all'uscita (moroso):

# 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 } ] }'