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.
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:
- 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:
AllowGroupPurchaseyMaxGroupSizeen TicketArticleConfigGroupOrderIdconecta todas las entradas de un pedidoGroupIndex(basado en 1) yGroupTotalpara el seguimientoCancelTicketGroupCommandanula todo el grupoGuestNamesopcional 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 entradaEnterRuleId→ Regla de entrada específicaFreeArticles→ 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 |