Saltar a contenido
v26.3

API de Disco (club sin efectivo)

El módulo Disco representa la operación de un club o discoteca gestionada de forma completamente sin efectivo: cada invitado recibe una tarjeta (chip/RFID/NFC) al entrar, que sirve como cuenta durante toda su estancia. Todo —entrada, bebidas, guardarropa, pedidos de mesa— se carga en la tarjeta y solo se liquida al salir (o mediante recarga de saldo prepago). La API cubre el ciclo de vida completo de una tarjeta: entrar → cargar → recargar → liquidar → salir.

Todos los endpoints están bajo el prefijo común api/v1/disco (tres controladores comparten este prefijo: invitados/tarjetas, estadísticas, configuración — además de la API de hardware del torniquete bajo api/v1/disco/turnstile). Como en todas las áreas: JWT Bearer, envoltorio de respuesta { "success": true, "data": { ... } }, campos en camelCase, IDs opacos.

Requiere el módulo Disco

Requiere el módulo Disco; sin una licencia de módulo activa, los endpoints devuelven HTTP 403 (errorCode: "MODULE_NOT_LICENSED").

ID de tarjeta frente a ID interna

Cada registro tiene una id interna (clave de base de datos) y una guestId — este es el número de tarjeta con el que está grabado físicamente el chip. Todas las rutas {cardId} esperan la guestId, no la id interna.

Invitados y tarjetas

DiscoController — base api/v1/disco:

Método Endpoint Descripción
POST /enter Dejar entrar al invitado en el torniquete (escaneo de tarjeta en la entrada)
POST /leave Hacer el check-out del invitado con pago (escaneo en la salida)
POST /leave-combined Liquidar y cerrar juntas varias tarjetas («pareja») en un solo recibo
POST /book Cargar artículo en la tarjeta (barra/mostrador)
POST /book/batch Cargar varios artículos a la vez en la tarjeta
POST /book/{guestId}/{bonIndex}/void Anular una posición cargada
POST /table/book-on-card Cargar un pedido de mesa en la tarjeta (sustituye el pago en la mesa)
POST /wardrobe/return/{guestId}/{bonIndex} Devolución de guardarropa (elimina el n.º de etiqueta, el importe se mantiene)
GET /guest/{cardId} Invitado/tarjeta incl. consumo actual (solo tarjetas abiertas)
GET /guest/{cardId}/status Estado de la tarjeta, también si está bloqueada/anulada/liquidada
GET /guest/{cardId}/checkout Vista de liquidación: importe pendiente, cobertura del prepago, si puede salir
GET /guests Todos los invitados actualmente presentes (con check-in)
POST /guest/{cardId}/topup Recargar saldo prepago
POST /guest/{cardId}/transfer Transferir tarjeta/saldo a otra tarjeta (cambio de tarjeta)
POST /guest/{cardId}/lock Bloquear la tarjeta (ya no se pueden hacer cargos)
POST /guest/{cardId}/unlock Desbloquear la tarjeta
POST /guest/{cardId}/storno Anular toda la tarjeta y marcarla como impagada
POST /guest/{cardId}/collect-storno Cobrar posteriormente en caja una tarjeta anulada
POST /guest/{cardId}/image Guardar la foto de la tarjeta
POST /terminal/pay Iniciar un pago con tarjeta de autoservicio en el terminal de información
POST /personal/{cardId}/close Liquidar una tarjeta de personal (autoconsumo)
POST /personal/close-all Liquidar todas las tarjetas de personal abiertas como autoconsumo
POST /close-all Cerrar todas las tarjetas abiertas (fin de servicio/cierre de caja)

Cargar en la tarjeta

POST /api/v1/disco/book carga un artículo en una tarjeta abierta (venta de barra/mostrador sin pago — el pago se realiza solo en 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"
}

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

Estado de la tarjeta

GET /api/v1/disco/guest/{cardId}/status devuelve el estado con independencia de si la tarjeta sigue abierta — a diferencia de GET /guest/{cardId}, que solo encuentra tarjetas abiertas. Así, la barra, el guardarropa o la caja también reconocen tarjetas bloqueadas o anuladas.

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 actual

GET /api/v1/disco/guest/{cardId} devuelve la misma DiscoGuestResponse que arriba, incl. los cargos abiertos en curso (openBons) y el saldo prepago (uploadAccount). Respuesta 404 GUEST_NOT_FOUND si la tarjeta ya no está abierta.

Liquidación/Check-out

GET /api/v1/disco/guest/{cardId}/checkout no modifica nada — es solo una vista previa de lo que la tarjeta aún debe, lo que cubre el saldo prepago y si puede salir automáticamente (autocobro, caja de salida móvil, torniquete).

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

Mesa en la tarjeta

POST /api/v1/disco/table/book-on-card traslada las posiciones abiertas de una mesa a una tarjeta Disco (modo mesa del Disco) — sustituye el pago en efectivo/tarjeta en la mesa, los límites de la tarjeta siguen aplicándose y la mesa queda libre.

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

openBonIds: null transfiere todas las posiciones abiertas; una lista transfiere solo una ronda «inmediata».

Recargar

POST /api/v1/disco/guest/{cardId}/topup recarga el saldo prepago de la tarjeta (uploadAccount) — el importe se puede elegir libremente y un saldo restante se reembolsa en el checkout.

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

La respuesta es de nuevo la DiscoGuestResponse actualizada (uploadAccount incrementado).

Riesgo de confusión: saldo de tarjeta Disco frente a saldo de cuenta de cliente

Esto es el prepago de tarjeta de una única estancia en el Disco (caduca/se reembolsa al salir). Un saldo de cuenta de cliente permanente es un concepto propio — véase Cuenta de cliente: saldo y puntos.

Estadísticas

DiscoStatsController — base api/v1/disco:

Método Endpoint Descripción
GET /stats Indicadores en vivo (ingresos, invitados presentes, ocupación por grupo/puesto de trabajo/personal/hora)
GET /search Buscar tarjetas/invitados (número de tarjeta, nombre, periodo, sexo, filtro de anulación, paginación)
GET /personal/{ownerId}/activity Actividad de una tarjeta de personal (consumo, anulación, rotura)
GET /daylog/{date} Registro diario para una fecha

Ejemplo: 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 admite, entre otros, cardId, name, dateFrom/dateTo, gender, timeFrom/timeTo, workplaceId, enterGroupId, wardrobe, stornoed, offset/limit, excludeImages.

Configuración

DiscoConfigController — base api/v1/disco, CRUD clásico para los grupos de entrada/precio («Entergroups» — definen, p. ej., el precio de entrada, el límite de tarjeta, las bebidas gratuitas y el consumo mínimo):

Método Endpoint Descripción
GET /entergroups Todos los grupos de entrada
GET /entergroups/{id} Un solo grupo de entrada
POST /entergroups Crear grupo de entrada
PUT /entergroups/{id} Actualizar grupo de entrada
DELETE /entergroups/{id} Eliminar grupo de entrada

Hardware del torniquete

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

Sin JWT — la autenticación se realiza mediante una clave API en la URL ({key}), para que un controlador de torniquete sencillo pueda llamar a la ruta sin flujo de inicio de sesión. 200 = la tarjeta puede salir, cualquier otro código indica un motivo (bloqueada/abierta/ya liquidada/ desconocida/clave incorrecta).

→ Detalles, configuración y tabla de códigos de estado: Torniquete de discoteca.

Flujo de extremo a extremo

Un recorrido típico de invitado, todos los pasos contra crd_10042:

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

# 1. Entrada en el torniquete (escaneo de tarjeta en la entrada)
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. Comprobar el estado (p. ej. reentrada tras una breve salida)
curl "$API/disco/guest/crd_10042/status" -H "Authorization: Bearer $TOKEN"

# 3. Recargar el prepago
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. Cargar en la barra (individual o por lotes)
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. Consultar el consumo actual
curl "$API/disco/guest/crd_10042" -H "Authorization: Bearer $TOKEN"

# 6. Cargar el pedido de mesa en la tarjeta
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. Antes de la salida: vista previa del checkout
curl "$API/disco/guest/crd_10042/checkout" -H "Authorization: Bearer $TOKEN"

# 8. Salir con pago (si aún queda algo pendiente)
curl -X POST "$API/disco/leave" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "cardId": "crd_10042", "payments": [ { "method": "Cash", "amount": 0 } ] }'

Incidencia — la tarjeta no puede pagar en la salida (impago):

# Anular la tarjeta: marcarla como impagada, contabilizar el consumo como merma
curl -X POST "$API/disco/guest/crd_10042/storno" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "reason": "Karte verloren" }'

# Cobrada posteriormente en caja
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 } ] }'