Saltar a contenido
v26.3

Sistema de venta de entradas en línea

El sistema de venta de entradas permite la venta en línea y la gestión de entradas para eventos (discoteca, eventos) con acceso basado en código QR, gestión de aforo e integración en el cierre de caja.

Arquitectura

La venta de entradas está implementada como paquete de funciones (Dikas.Features.Ticketing), de forma análoga al módulo de discoteca. Utiliza la interfaz IFeatureModule y se integra en la aplicación principal mediante DI.

Dikas.Features.Ticketing/
├── Entities/           # Ticket, TicketArticleConfig
├── Commands/           # 10 Commands (CQRS)
├── Queries/            # 12 Queries (CQRS)
├── Contracts/          # DTOs (Request/Response)
├── Controllers/        # 3 Controller (33 Endpoints)
├── Services/           # QR, PDF, ExpireJob, DayCloseStats
├── Validators/         # FluentValidation
└── TicketingModule.cs  # DI-Registrierung

Entidades

Ticket

SoftDeletableDocument, DocumentType "Ticket", prefijo de ID tkt_, cacheAll: false

Campo Tipo Descripción
TicketNumber string Número legible (TKT-YYYYMMDD-XXXXXX)
QrCode string Código QR único (GUID)
Name string Nombre del evento/entrada
Status int 0=Purchased, 1=CheckedIn, 2=Cancelled, 3=Expired, 4=Refunded
ArticleId string Referencia al artículo de entrada
EventDate DateTime Fecha del evento
TimeWindowStart/End string? Franja horaria de acceso (HH:MM)
ExpiresAt DateTime Fecha de caducidad (EventDate + 1 día)
EnterGroupId string? Grupo de entrada de discoteca
EnterRuleId string? Regla de entrada de discoteca
AssignedCardId string? Tarjeta NFC asignada en el check-in
FreeDrinks int Bebidas gratuitas incluidas
FreeAccount decimal Saldo incluido
FreeArticles List\<FreeArticleItem> Artículos gratuitos incluidos
IncludesWardrobe bool Guardarropa incluido
GuestName string? Nombre personalizado del invitado
GuestEmail string? Correo electrónico del invitado
IsPersonalized bool Obligatoriedad de personalización
GroupOrderId string? ID de pedido de grupo
GroupIndex/GroupTotal int? Posición/total en el grupo
Price decimal Precio de la entrada
PaidAmount decimal Importe ya pagado
RemainingAmount decimal Importe restante pendiente
RefundAmount decimal? Importe del reembolso
RefundMethod string? Método de reembolso
PaymentProvider string Proveedor de pago (stripe, paypal, etc.)
PaymentTransactionId string ID de transacción externo
TaxClass int Clase fiscal
CheckedInAt/By/WorkplaceId DateTime?/string? Datos del check-in
CancelledAt/By/CancelReason DateTime?/string? Datos de la anulación
RefundedAt DateTime? Momento del reembolso
StatusHistory List\<TicketStatusEntry> Pista de auditoría de los cambios de estado
OrderId string ID de pedido de la tienda
CustomerId string? Referencia del cliente

TicketArticleConfig

BaseDocument, DocumentType "TicketArticleConfig", prefijo de ID tktcfg_, cacheAll: true

Configura un artículo como tipo de entrada con todos los valores predeterminados y reglas.

Campo Tipo Descripción
ArticleId string Artículo vinculado
EnterGroupId string? Grupo de entrada de discoteca predeterminado
EnterRuleId string? Regla de entrada de discoteca predeterminada
FreeDrinks int Bebidas gratuitas predeterminadas
FreeAccount decimal Saldo gratuito predeterminado
FreeArticles List\<FreeArticleItem> Artículos gratuitos predeterminados
IncludesWardrobe bool Guardarropa incluido
MaxCapacityPerDate int? Máx. de entradas por fecha
OverbookingPercent int? Sobreventa en % (p. ej. 10 = +10 %)
CancelDeadlineHours int? Plazo de anulación en horas antes del evento
AllowRefund bool Reembolso permitido
IsPersonalized bool Obligatoriedad de personalización
AllowGroupPurchase bool Compra en grupo permitida
MaxGroupSize int Tamaño máx. de grupo (predeterminado: 10)
TimeWindowStart/End string? Franja horaria de acceso predeterminada
OnlineOnly bool Disponible solo en la tienda en línea
AllowPartialPayment bool Pago parcial/anticipo permitido
MinPrepayment decimal Anticipo mínimo
SoldPerDate Dictionary\<string, int> Entradas vendidas por fecha (yyyy-MM-dd → cantidad)

Endpoints de la API

TicketController — /api/v1/tickets [Authorize]

Entradas:

Method Endpoint Descripción
GET /{id} Entrada por ID
GET /by-qr/{qrCode} Entrada por código QR
GET /by-date?date= Entradas por fecha
GET /by-order/{orderId} Entradas de un pedido
GET /by-customer/{customerId} Entradas de un cliente
GET /search?search=&status=&dateFrom=&dateTo=&limit= Buscar entradas
GET /capacity?date=&articleId= Consultar aforos
GET /stats?dateFrom=&dateTo= Estadísticas
POST /validate Validar código QR (sin check-in)
POST /check-in Realizar el acceso por QR
POST /{id}/cancel Anular entrada
POST /{id}/pay Pagar el importe restante
POST /{id}/refund Realizar reembolso
POST /cancel-group/{groupOrderId} Anular pedido de grupo

Configuraciones:

Method Endpoint Descripción
GET /configs Todas las configuraciones de entradas
GET /configs/{id} Configuración por ID
POST /configs Crear configuración (201)
PUT /configs/{id} Actualizar configuración
DELETE /configs/{id} Eliminar configuración

TicketPortalController — /api/v1/portal/tickets [Authorize(Policy="Portal")]

Method Endpoint Descripción
GET /my?customerId= Entradas propias del cliente
POST /{id}/cancel El cliente anula su propia entrada
GET /{id}/qr Código QR como PNG [AllowAnonymous]
GET /{id}/pdf Entrada como PDF [AllowAnonymous]

TicketShopController — /api/v1/ticket-shop [AllowAnonymous]

Method Endpoint Descripción
GET /catalog Tipos de entrada disponibles
GET /capacity?date=&articleId= Comprobar disponibilidad
POST /validate Validar el checkout
POST /purchase Comprar entradas

Commands

Command Devolución Descripción
CreateTicketCommand List\<TicketResponse> Crea 1 o más entradas, genera QR + número, envía correo de confirmación con PDF
CheckInTicketCommand CheckInResult Valida el QR, establece el estado en CheckedIn, registra la marca de tiempo
CancelTicketCommand TicketResponse Anula la entrada (solo Status=Purchased), comprueba el plazo de anulación, envía correo de anulación
ProcessTicketPaymentCommand TicketResponse Contabiliza el pago restante, actualiza PaidAmount/RemainingAmount
RefundTicketCommand TicketResponse Reembolso (solo Status=Cancelled), comprueba AllowRefund
CancelTicketGroupCommand List\<TicketResponse> Anula todas las entradas de un pedido de grupo
ValidateTicketCheckoutCommand TicketCheckoutValidationResult Comprobación de aforo y configuración para la tienda
CreateTicketArticleConfigCommand TicketArticleConfigResponse Crear nueva configuración de entrada
UpdateTicketArticleConfigCommand TicketArticleConfigResponse Actualizar configuración
DeleteTicketArticleConfigCommand bool Eliminar configuración

Queries

Query Devolución Descripción
GetTicketByIdQuery TicketResponse Una sola entrada
GetTicketByQrCodeQuery TicketResponse Buscar entrada por código QR
GetTicketsByDateQuery List\<TicketResponse> Entradas por fecha del evento
GetTicketsByCustomerQuery List\<TicketResponse> Entradas de un cliente
GetTicketsByOrderQuery List\<TicketResponse> Entradas de un pedido
SearchTicketsQuery List\<TicketResponse> Búsqueda de texto completo con filtros
GetTicketCapacityQuery List\<TicketCapacityResponse> Aforo por artículo/fecha
GetTicketStatsQuery TicketStatsResponse Estadísticas agregadas
ValidateTicketQuery CheckInResult Validación del check-in sin efectos secundarios
GetTicketArticleConfigsQuery List\<TicketArticleConfigResponse> Todas las configuraciones
GetTicketArticleConfigQuery TicketArticleConfigResponse Una sola configuración
GetTicketCatalogQuery List\<TicketShopItemResponse> Catálogo de la tienda en línea

Services

TicketQrService (Singleton)

Genera códigos QR con la biblioteca QRCoder.

interface ITicketQrService {
    byte[] GeneratePng(string content, int pixelsPerModule = 10);
    string GenerateSvg(string content);
}

TicketPdfService (Scoped)

Crea PDF de entradas en formato A6 con QuestPDF. Contiene un código QR incrustado, los detalles de la entrada e información opcional del recinto.

interface ITicketPdfService {
    byte[] GenerateTicketPdf(Ticket ticket, string? venueInfo = null);
}

ExpireTicketsJob (HostedService)

Trabajo en segundo plano que se ejecuta cada 60 minutos y establece automáticamente en Status=Expired las entradas caducadas (Status=Purchased, ExpiresAt < ahora).

DayCloseTicketStatsProvider (Scoped)

Proporciona estadísticas de entradas para el cierre de caja:

interface IDayCloseTicketStatsProvider {
    Task<DayCloseTicketStats> GetStatsForPeriodAsync(DateTime from, DateTime to, CancellationToken ct);
}

Devolución: SoldOnlineCount/Amount, CheckedInCount, RefundedCount/Amount

Dependencia opcional entre módulos: NullDayCloseTicketStatsProvider registrado como predeterminado, sobrescrito por el TicketingModule con la implementación real.

Flujo de check-in

QR-Code scannen
ValidateTicketQuery
    ├── Ticket nicht gefunden → Invalid
    ├── Status != Purchased → AlreadyUsed / Cancelled / Expired
    ├── EventDate != heute → WrongDate
    ├── Außerhalb TimeWindow → OutsideTimeWindow
    ├── RemainingAmount > 0 → HasRemainingAmount (Warnung, Einlass möglich)
    └── Alles OK → Valid
CheckInTicketCommand
    ├── Status → CheckedIn
    ├── CheckedInAt/By/WorkplaceId setzen
    ├── AssignedCardId setzen (optional, für Disco-Karte)
    └── StatusHistory-Eintrag erstellen

Enum CheckInStatus:

Valor Significado
0 - Valid Entrada válida, acceso permitido
1 - Invalid Entrada no válida/no encontrada
2 - Expired Entrada caducada
3 - WrongDate Fecha de evento incorrecta
4 - OutsideTimeWindow Fuera de la franja horaria de acceso
5 - AlreadyUsed Ya validada
6 - Cancelled Entrada anulada
7 - NameMismatch Discrepancia de nombre (entrada personalizada)
8 - HasRemainingAmount Importe restante pendiente (acceso posible igualmente)

Flujo de checkout de la tienda en línea

1. GET /ticket-shop/catalog
   → Liste verfügbarer Ticket-Typen

2. GET /ticket-shop/capacity?date=2026-03-15
   → Verfügbarkeit für gewähltes Datum

3. POST /ticket-shop/validate
   { articleId, eventDate, quantity }
   → Kapazitätsprüfung, Preisberechnung

4. Zahlung extern (Stripe.js / PayPal SDK)

5. POST /ticket-shop/purchase
   { articleId, eventDate, quantity, customerEmail, customerName, paymentProvider }
   → Tickets erstellen, Bestätigungs-Email mit PDF-Tickets

La comprobación de aforo tiene en cuenta MaxCapacityPerDate y OverbookingPercent:

Effektive Kapazität = MaxCapacity + (MaxCapacity × OverbookingPercent / 100)
Verfügbar = Effektive Kapazität - SoldPerDate[datum]

Gestión de aforo

Las entradas vendidas se almacenan en TicketArticleConfig.SoldPerDate como diccionario:

{
  "2026-03-15": 42,
  "2026-03-16": 85
}
  • Sin MaxCapacityPerDate: Disponibilidad ilimitada
  • Con MaxCapacityPerDate: Límite estricto por fecha
  • Con OverbookingPercent: Permite una sobreventa controlada (p. ej. 10 % = 110 con un máx. de 100)

Compra en grupo

Las entradas se pueden comprar en grupo:

  • AllowGroupPurchase y MaxGroupSize en TicketArticleConfig
  • GroupOrderId conecta todas las entradas de un pedido
  • GroupIndex (basado en 1) y GroupTotal para el seguimiento
  • CancelTicketGroupCommand anula todo el grupo
  • GuestNames opcional para entradas de grupo personalizadas

Integración con la discoteca

Las configuraciones de entradas se pueden vincular con grupos de entrada de discoteca:

  • EnterGroupId → Asignación automática al comprar la entrada
  • EnterRuleId → Regla de entrada específica
  • FreeArticles → Artículos gratuitos del módulo de discoteca
  • En el check-in: se puede asignar la tarjeta de discoteca (AssignedCardId)

Integración con el cierre de caja

El IDayCloseTicketStatsProvider proporciona los siguientes indicadores para el cierre de caja:

Indicador Descripción
SoldOnlineCount Entradas vendidas en línea en el periodo
SoldOnlineAmount Ventas totales de entradas vendidas en línea
CheckedInCount Entradas validadas en el periodo
RefundedCount Entradas reembolsadas en el periodo
RefundedAmount Importe total de los reembolsos

Integración de correo electrónico

Dos plantillas de correo electrónico:

  • ticket_confirmation: Se envía tras la compra de la entrada, contiene la entrada en PDF como adjunto
  • ticket_cancellation: Se envía tras la anulación

Campos de OperationalConfig

Campo Tipo Predeterminado Descripción
TicketingEnabled bool false Módulo de venta de entradas activado
TicketScanInDirectSale bool true El POS puede escanear entradas

Componentes de frontend

Admin — Página de entradas (/admin/tickets)

4 pestañas: 1. Entradas: Búsqueda, filtro de estado, rango de fechas, tabla con TicketNumber/Name/EventDate/Status/Price, acción de anulación, enlace al PDF 2. Aforos: Selección de fecha, tarjetas de aforo con barra de progreso (Vendido/Máx./Disponible) 3. Estadísticas: Tarjetas de indicadores (Vendido, Validado, Pendiente, Anulado, Caducado, Reembolsado, Ventas) 4. Configuración: Tabla de todas las configuraciones de artículos de entrada

Admin — Editor de artículos (pestaña de entradas)

Se muestra cuando ExtraOption === 15 (tipo de entrada): - Vinculación con discoteca (desplegable de EnterGroup) - Extras (bebidas gratuitas, saldo gratuito, guardarropa) - Contingente (MaxCapacity, sobreventa) - Reglas de anulación (plazo, reembolso) - Opciones (personalización, compra en grupo, franja horaria, pago parcial)

POS — Escaneo de entradas (/pos/ticket-scan)

  • Campo de entrada de código QR con enfoque automático
  • Resultado de validación con estado de color (verde/naranja/rojo)
  • Detalles de la entrada (número, nombre, fecha, invitado, extras)
  • Botón de acceso si la entrada es válida
  • Aviso de importe restante en caso de pago pendiente

Discoteca — Acceso (sección de entrada por QR)

  • Entrada de código QR debajo del escaneo de tarjeta
  • Validación y check-in con asignación de tarjeta
  • Visible solo si TicketingEnabled

Tienda en línea — Catálogo de entradas

  • Tarjetas de entrada con precio, ventajas (bebidas gratuitas, saldo, guardarropa), selección de fecha
  • Integración con el carrito con la nota ticket:{date}

Registro de extensiones

// Admin-Sidebar
{ id: 'tickets', label: 'Tickets', icon: 'fa-solid fa-ticket', order: 115 }

// POS-Menü
{ id: 'ticket-scan', name: 'Ticket-Scan', icon: 'fa-solid fa-qrcode', order: 120 }

Registro de DI (TicketingModule)

// Entities
Ticket:              cacheAll=false, BackupCategory="tickets"
TicketArticleConfig: cacheAll=true,  BackupCategory="tickets"

// Services
ITicketQrService             TicketQrService (Singleton)
ITicketPdfService            TicketPdfService (Scoped)
IDayCloseTicketStatsProvider  TicketDayCloseStatsProvider (Scoped)
ExpireTicketsJob             HostedService

// MediatR + FluentValidation
services.AddMediatR(...)
services.AddValidatorsFromAssembly(...)

Base de datos: Admite CouchDB y EF Core (SQLite/SQL Server). Configuración de EF Core con restricciones String-MaxLength y Decimal-Precision(18,4). SoldPerDate se almacena como columna JSON-TEXT.

Prefijos de ID (IdGenerator): "Ticket" → "tkt", "TicketArticleConfig" → "tktcfg"

Dependencias

Paquete Uso
QRCoder Generación de códigos QR (PNG, SVG)
QuestPDF Creación de entradas en PDF (formato A6)
MediatR Patrón CQRS
FluentValidation Validación de solicitudes