Sistema di ticketing online¶
Il sistema di ticketing consente la vendita online e la gestione di biglietti per eventi (Disco, manifestazioni) con ingresso basato su codice QR, gestione della capienza e integrazione nella chiusura giornaliera.
Architettura¶
Il ticketing è implementato come Feature-Pack (Dikas.Features.Ticketing), in modo analogo al modulo Disco. Utilizza l'interfaccia IFeatureModule e viene integrato nell'applicazione principale tramite 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
Entities¶
Ticket¶
SoftDeletableDocument, DocumentType "Ticket", prefisso ID tkt_, cacheAll: false
| Campo | Tipo | Descrizione |
|---|---|---|
| TicketNumber | string | Numero leggibile (TKT-YYYYMMDD-XXXXXX) |
| QrCode | string | Codice QR univoco (GUID) |
| Name | string | Nome evento/biglietto |
| Status | int | 0=Purchased, 1=CheckedIn, 2=Cancelled, 3=Expired, 4=Refunded |
| ArticleId | string | Riferimento all'articolo-biglietto |
| EventDate | DateTime | Data dell'evento |
| TimeWindowStart/End | string? | Fascia oraria d'ingresso (HH:MM) |
| ExpiresAt | DateTime | Data di scadenza (EventDate + 1 giorno) |
| EnterGroupId | string? | Gruppo d'ingresso Disco |
| EnterRuleId | string? | Regola d'ingresso Disco |
| AssignedCardId | string? | Carta NFC assegnata al check-in |
| FreeDrinks | int | Bevande gratuite incluse |
| FreeAccount | decimal | Credito incluso |
| FreeArticles | List\<FreeArticleItem> | Articoli gratuiti inclusi |
| IncludesWardrobe | bool | Guardaroba incluso |
| GuestName | string? | Nome ospite personalizzato |
| GuestEmail | string? | Email ospite |
| IsPersonalized | bool | Obbligo di personalizzazione |
| GroupOrderId | string? | ID ordine di gruppo |
| GroupIndex/GroupTotal | int? | Posizione/totale nel gruppo |
| Price | decimal | Prezzo del biglietto |
| PaidAmount | decimal | Importo già pagato |
| RemainingAmount | decimal | Importo residuo da pagare |
| RefundAmount | decimal? | Importo di rimborso |
| RefundMethod | string? | Metodo di rimborso |
| PaymentProvider | string | Provider di pagamento (stripe, paypal, ecc.) |
| PaymentTransactionId | string | ID transazione esterna |
| TaxClass | int | Classe fiscale |
| CheckedInAt/By/WorkplaceId | DateTime?/string? | Dati di check-in |
| CancelledAt/By/CancelReason | DateTime?/string? | Dati di storno |
| RefundedAt | DateTime? | Momento del rimborso |
| StatusHistory | List\<TicketStatusEntry> | Audit trail delle modifiche di stato |
| OrderId | string | ID ordine del negozio |
| CustomerId | string? | Riferimento al cliente |
TicketArticleConfig¶
BaseDocument, DocumentType "TicketArticleConfig", prefisso ID tktcfg_, cacheAll: true
Configura un articolo come tipo di biglietto con tutti i valori predefiniti e le regole.
| Campo | Tipo | Descrizione |
|---|---|---|
| ArticleId | string | Articolo collegato |
| EnterGroupId | string? | Gruppo d'ingresso Disco predefinito |
| EnterRuleId | string? | Regola d'ingresso Disco predefinita |
| FreeDrinks | int | Bevande gratuite predefinite |
| FreeAccount | decimal | Credito gratuito predefinito |
| FreeArticles | List\<FreeArticleItem> | Articoli gratuiti predefiniti |
| IncludesWardrobe | bool | Guardaroba incluso |
| MaxCapacityPerDate | int? | Max. biglietti per data |
| OverbookingPercent | int? | Overbooking in % (ad es. 10 = +10%) |
| CancelDeadlineHours | int? | Termine di storno in ore prima dell'evento |
| AllowRefund | bool | Rimborso consentito |
| IsPersonalized | bool | Obbligo di personalizzazione |
| AllowGroupPurchase | bool | Acquisto di gruppo consentito |
| MaxGroupSize | int | Dimensione massima del gruppo (predefinito: 10) |
| TimeWindowStart/End | string? | Fascia oraria d'ingresso predefinita |
| OnlineOnly | bool | Disponibile solo nel negozio online |
| AllowPartialPayment | bool | Pagamento parziale/acconto consentito |
| MinPrepayment | decimal | Acconto minimo |
| SoldPerDate | Dictionary\<string, int> | Biglietti venduti per data (yyyy-MM-dd → quantità) |
Endpoint API¶
TicketController — /api/v1/tickets [Authorize]¶
Tickets:
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /{id} |
Biglietto per ID |
| GET | /by-qr/{qrCode} |
Biglietto per codice QR |
| GET | /by-date?date= |
Biglietti per data |
| GET | /by-order/{orderId} |
Biglietti di un ordine |
| GET | /by-customer/{customerId} |
Biglietti di un cliente |
| GET | /search?search=&status=&dateFrom=&dateTo=&limit= |
Cercare biglietti |
| GET | /capacity?date=&articleId= |
Interrogare le capienze |
| GET | /stats?dateFrom=&dateTo= |
Statistiche |
| POST | /validate |
Validare il codice QR (senza check-in) |
| POST | /check-in |
Eseguire l'ingresso via QR |
| POST | /{id}/cancel |
Stornare un biglietto |
| POST | /{id}/pay |
Pagare l'importo residuo |
| POST | /{id}/refund |
Eseguire il rimborso |
| POST | /cancel-group/{groupOrderId} |
Stornare un ordine di gruppo |
Configurazioni:
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /configs |
Tutte le configurazioni dei biglietti |
| GET | /configs/{id} |
Configurazione per ID |
| POST | /configs |
Creare una configurazione (201) |
| PUT | /configs/{id} |
Aggiornare una configurazione |
| DELETE | /configs/{id} |
Eliminare una configurazione |
TicketPortalController — /api/v1/portal/tickets [Authorize(Policy="Portal")]¶
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /my?customerId= |
Biglietti propri del cliente |
| POST | /{id}/cancel |
Il cliente storna il proprio biglietto |
| GET | /{id}/qr |
Codice QR come PNG [AllowAnonymous] |
| GET | /{id}/pdf |
Biglietto come PDF [AllowAnonymous] |
TicketShopController — /api/v1/ticket-shop [AllowAnonymous]¶
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /catalog |
Tipi di biglietto disponibili |
| GET | /capacity?date=&articleId= |
Verificare la disponibilità |
| POST | /validate |
Validare il checkout |
| POST | /purchase |
Acquistare biglietti |
Commands¶
| Command | Restituisce | Descrizione |
|---|---|---|
| CreateTicketCommand | List\<TicketResponse> | Crea 1+ biglietti, genera QR + numero, invia email di conferma con PDF |
| CheckInTicketCommand | CheckInResult | Valida il QR, imposta lo stato su CheckedIn, registra il timestamp |
| CancelTicketCommand | TicketResponse | Storna il biglietto (solo Status=Purchased), verifica il termine di storno, invia email di storno |
| ProcessTicketPaymentCommand | TicketResponse | Registra il pagamento residuo, aggiorna PaidAmount/RemainingAmount |
| RefundTicketCommand | TicketResponse | Rimborso (solo Status=Cancelled), verifica AllowRefund |
| CancelTicketGroupCommand | List\<TicketResponse> | Storna tutti i biglietti di un ordine di gruppo |
| ValidateTicketCheckoutCommand | TicketCheckoutValidationResult | Verifica di capienza e configurazione per il negozio |
| CreateTicketArticleConfigCommand | TicketArticleConfigResponse | Creare una nuova configurazione del biglietto |
| UpdateTicketArticleConfigCommand | TicketArticleConfigResponse | Aggiornare la configurazione |
| DeleteTicketArticleConfigCommand | bool | Eliminare la configurazione |
Queries¶
| Query | Restituisce | Descrizione |
|---|---|---|
| GetTicketByIdQuery | TicketResponse | Singolo biglietto |
| GetTicketByQrCodeQuery | TicketResponse | Cercare un biglietto per codice QR |
| GetTicketsByDateQuery | List\<TicketResponse> | Biglietti per data dell'evento |
| GetTicketsByCustomerQuery | List\<TicketResponse> | Biglietti di un cliente |
| GetTicketsByOrderQuery | List\<TicketResponse> | Biglietti di un ordine |
| SearchTicketsQuery | List\<TicketResponse> | Ricerca full-text con filtri |
| GetTicketCapacityQuery | List\<TicketCapacityResponse> | Capienza per articolo/data |
| GetTicketStatsQuery | TicketStatsResponse | Statistiche aggregate |
| ValidateTicketQuery | CheckInResult | Validazione del check-in senza effetti collaterali |
| GetTicketArticleConfigsQuery | List\<TicketArticleConfigResponse> | Tutte le configurazioni |
| GetTicketArticleConfigQuery | TicketArticleConfigResponse | Singola configurazione |
| GetTicketCatalogQuery | List\<TicketShopItemResponse> | Catalogo del negozio online |
Services¶
TicketQrService (Singleton)¶
Genera codici QR con la libreria QRCoder.
interface ITicketQrService {
byte[] GeneratePng(string content, int pixelsPerModule = 10);
string GenerateSvg(string content);
}
TicketPdfService (Scoped)¶
Crea i PDF dei biglietti in formato A6 con QuestPDF. Contiene il codice QR incorporato, i dettagli del biglietto e le informazioni opzionali sulla sede.
ExpireTicketsJob (HostedService)¶
Job in background che viene eseguito ogni 60 minuti e imposta automaticamente i biglietti scaduti (Status=Purchased, ExpiresAt < adesso) sullo stato Status=Expired.
DayCloseTicketStatsProvider (Scoped)¶
Fornisce le statistiche dei biglietti per la chiusura giornaliera:
interface IDayCloseTicketStatsProvider {
Task<DayCloseTicketStats> GetStatsForPeriodAsync(DateTime from, DateTime to, CancellationToken ct);
}
Restituisce: SoldOnlineCount/Amount, CheckedInCount, RefundedCount/Amount
Dipendenza cross-modulo opzionale: NullDayCloseTicketStatsProvider registrato come default, viene sovrascritto dal TicketingModule con l'implementazione reale.
Flusso di 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:
| Valore | Significato |
|---|---|
| 0 - Valid | Biglietto valido, ingresso consentito |
| 1 - Invalid | Biglietto non valido/non trovato |
| 2 - Expired | Biglietto scaduto |
| 3 - WrongDate | Data dell'evento errata |
| 4 - OutsideTimeWindow | Fuori dalla fascia oraria d'ingresso |
| 5 - AlreadyUsed | Già registrato all'ingresso |
| 6 - Cancelled | Biglietto stornato |
| 7 - NameMismatch | Discordanza di nome (biglietto personalizzato) |
| 8 - HasRemainingAmount | Importo residuo da pagare (ingresso comunque possibile) |
Flusso di checkout del negozio online¶
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 verifica della capienza tiene conto di MaxCapacityPerDate e OverbookingPercent:
Effektive Kapazität = MaxCapacity + (MaxCapacity × OverbookingPercent / 100)
Verfügbar = Effektive Kapazität - SoldPerDate[datum]
Gestione della capienza¶
I biglietti venduti vengono memorizzati in TicketArticleConfig.SoldPerDate come dizionario:
- Senza MaxCapacityPerDate: disponibilità illimitata
- Con MaxCapacityPerDate: limite rigido per data
- Con OverbookingPercent: consente un overbooking controllato (ad es. 10% = 110 con Max 100)
Acquisto di gruppo¶
I biglietti possono essere acquistati come gruppo:
AllowGroupPurchaseeMaxGroupSizesu TicketArticleConfigGroupOrderIdcollega tutti i biglietti di un ordineGroupIndex(a base 1) eGroupTotalper il tracciamentoCancelTicketGroupCommandstorna l'intero gruppoGuestNamesopzionali per biglietti di gruppo personalizzati
Integrazione Disco¶
Le configurazioni dei biglietti possono essere collegate ai gruppi d'ingresso Disco:
EnterGroupId→ Assegnazione automatica all'acquisto del bigliettoEnterRuleId→ Regola d'ingresso specificaFreeArticles→ Articoli gratuiti dal modulo Disco- Al check-in: è possibile assegnare la carta Disco (
AssignedCardId)
Integrazione nella chiusura giornaliera¶
L'IDayCloseTicketStatsProvider fornisce i seguenti indicatori per la chiusura giornaliera:
| Indicatore | Descrizione |
|---|---|
| SoldOnlineCount | Biglietti venduti online nel periodo |
| SoldOnlineAmount | Fatturato totale dei biglietti venduti online |
| CheckedInCount | Biglietti registrati all'ingresso nel periodo |
| RefundedCount | Biglietti rimborsati nel periodo |
| RefundedAmount | Importo totale dei rimborsi |
Integrazione email¶
Due template email:
- ticket_confirmation: viene inviato dopo l'acquisto del biglietto, contiene il PDF del biglietto in allegato
- ticket_cancellation: viene inviato dopo lo storno
Campi OperationalConfig¶
| Campo | Tipo | Predefinito | Descrizione |
|---|---|---|---|
| TicketingEnabled | bool | false | Modulo ticketing attivato |
| TicketScanInDirectSale | bool | true | Il POS può scansionare i biglietti |
Componenti frontend¶
Admin — Pagina biglietti (/admin/tickets)¶
4 schede: 1. Tickets: ricerca, filtro per stato, intervallo di date, tabella con TicketNumber/Name/EventDate/Status/Price, azione di storno, link PDF 2. Capienze: selezione della data, schede di capienza con barra di avanzamento (Venduti/Max/Disponibili) 3. Statistiche: schede con indicatori (Venduti, Registrati all'ingresso, In sospeso, Stornati, Scaduti, Rimborsati, Fatturato) 4. Configurazione: tabella di tutte le configurazioni articolo-biglietto
Admin — Editor articolo (scheda Ticket)¶
Viene visualizzato quando ExtraOption === 15 (tipo di biglietto):
- Collegamento Disco (dropdown EnterGroup)
- Extra (bevande gratuite, credito gratuito, guardaroba)
- Contingente (MaxCapacity, Overbooking)
- Regole di storno (termine, rimborso)
- Opzioni (personalizzazione, acquisto di gruppo, fascia oraria, pagamento parziale)
POS — Scansione biglietto (/pos/ticket-scan)¶
- Campo di inserimento del codice QR con autofocus
- Risultato della validazione con stato colorato (verde/arancione/rosso)
- Dettagli del biglietto (numero, nome, data, ospite, extra)
- Pulsante d'ingresso per biglietto valido
- Avviso di importo residuo in caso di pagamento in sospeso
Disco — Ingresso (sezione QR-Ticket)¶
- Inserimento del codice QR sotto la scansione della carta
- Validazione e check-in con assegnazione della carta
- Visibile solo se
TicketingEnabled
Negozio online — Catalogo biglietti¶
- Schede biglietto con prezzo, perk (bevande gratuite, credito, guardaroba), selezione della data
- Integrazione nel carrello con nota
ticket:{date}
Registrazione dell'estensione¶
// 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 }
Registrazione 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(...)
Database: supporta CouchDB ed EF Core (SQLite/SQL Server). Configurazione EF Core con vincoli di String-MaxLength e Decimal-Precision(18,4). SoldPerDate viene memorizzato come colonna JSON-TEXT.
Prefissi ID (IdGenerator): "Ticket" → "tkt", "TicketArticleConfig" → "tktcfg"
Dipendenze¶
| Pacchetto | Utilizzo |
|---|---|
| QRCoder | Generazione codice QR (PNG, SVG) |
| QuestPDF | Creazione PDF dei biglietti (formato A6) |
| MediatR | Pattern CQRS |
| FluentValidation | Validazione delle richieste |