Zum Inhalt
v26.3

Online-Ticketing System

Das Ticketing-System ermöglicht den Online-Verkauf und die Verwaltung von Event-Tickets (Disco, Veranstaltungen) mit QR-Code-basiertem Einlass, Kapazitätsmanagement und Integration in den Tagesabschluss.

Architektur

Das Ticketing ist als Feature-Pack (Dikas.Features.Ticketing) implementiert, analog zum Disco-Modul. Es nutzt das IFeatureModule-Interface und wird per DI in die Hauptanwendung eingebunden.

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", ID-Prefix tkt_, cacheAll: false

Feld Typ Beschreibung
TicketNumber string Lesbare Nummer (TKT-YYYYMMDD-XXXXXX)
QrCode string Eindeutiger QR-Code (GUID)
Name string Event-/Ticketname
Status int 0=Purchased, 1=CheckedIn, 2=Cancelled, 3=Expired, 4=Refunded
ArticleId string Referenz zum Ticket-Artikel
EventDate DateTime Veranstaltungsdatum
TimeWindowStart/End string? Einlass-Zeitfenster (HH:MM)
ExpiresAt DateTime Ablaufdatum (EventDate + 1 Tag)
EnterGroupId string? Disco-Eintrittsgruppe
EnterRuleId string? Disco-Eintrittsregel
AssignedCardId string? NFC-Karte beim Check-in zugewiesen
FreeDrinks int Inkludierte Freigetränke
FreeAccount decimal Inkludiertes Guthaben
FreeArticles List\<FreeArticleItem> Inkludierte Freiartikel
IncludesWardrobe bool Garderobe inklusive
GuestName string? Personalisierter Gastname
GuestEmail string? Gast-Email
IsPersonalized bool Personalisierungspflicht
GroupOrderId string? Gruppen-Bestell-ID
GroupIndex/GroupTotal int? Position/Gesamt in Gruppe
Price decimal Ticketpreis
PaidAmount decimal Bereits bezahlter Betrag
RemainingAmount decimal Offener Restbetrag
RefundAmount decimal? Erstattungsbetrag
RefundMethod string? Erstattungsmethode
PaymentProvider string Zahlungsanbieter (stripe, paypal, etc.)
PaymentTransactionId string Externe Transaktions-ID
TaxClass int Steuerklasse
CheckedInAt/By/WorkplaceId DateTime?/string? Check-in-Daten
CancelledAt/By/CancelReason DateTime?/string? Stornierungsdaten
RefundedAt DateTime? Erstattungszeitpunkt
StatusHistory List\<TicketStatusEntry> Audit-Trail der Statusänderungen
OrderId string Shop-Bestell-ID
CustomerId string? Kundenreferenz

TicketArticleConfig

BaseDocument, DocumentType "TicketArticleConfig", ID-Prefix tktcfg_, cacheAll: true

Konfiguriert einen Artikel als Ticket-Typ mit allen Defaults und Regeln.

Feld Typ Beschreibung
ArticleId string Verknüpfter Artikel
EnterGroupId string? Standard-Disco-Eintrittsgruppe
EnterRuleId string? Standard-Disco-Eintrittsregel
FreeDrinks int Standard-Freigetränke
FreeAccount decimal Standard-Freiguthaben
FreeArticles List\<FreeArticleItem> Standard-Freiartikel
IncludesWardrobe bool Garderobe inklusive
MaxCapacityPerDate int? Max. Tickets pro Datum
OverbookingPercent int? Überbuchung in % (z.B. 10 = +10%)
CancelDeadlineHours int? Stornofrist in Stunden vor Event
AllowRefund bool Erstattung erlaubt
IsPersonalized bool Personalisierungspflicht
AllowGroupPurchase bool Gruppenkauf erlaubt
MaxGroupSize int Max. Gruppengröße (Standard: 10)
TimeWindowStart/End string? Standard-Einlass-Zeitfenster
OnlineOnly bool Nur im Online-Shop verfügbar
AllowPartialPayment bool Teilzahlung/Anzahlung erlaubt
MinPrepayment decimal Mindestanzahlung
SoldPerDate Dictionary\<string, int> Verkaufte Tickets pro Datum (yyyy-MM-dd → Anzahl)

API-Endpoints

TicketController — /api/v1/tickets [Authorize]

Tickets:

Method Endpoint Beschreibung
GET /{id} Ticket nach ID
GET /by-qr/{qrCode} Ticket nach QR-Code
GET /by-date?date= Tickets nach Datum
GET /by-order/{orderId} Tickets einer Bestellung
GET /by-customer/{customerId} Tickets eines Kunden
GET /search?search=&status=&dateFrom=&dateTo=&limit= Tickets suchen
GET /capacity?date=&articleId= Kapazitäten abfragen
GET /stats?dateFrom=&dateTo= Statistiken
POST /validate QR-Code validieren (ohne Check-in)
POST /check-in QR-Einlass durchführen
POST /{id}/cancel Ticket stornieren
POST /{id}/pay Restbetrag bezahlen
POST /{id}/refund Erstattung durchführen
POST /cancel-group/{groupOrderId} Gruppenbestellung stornieren

Konfigurationen:

Method Endpoint Beschreibung
GET /configs Alle Ticket-Konfigurationen
GET /configs/{id} Konfiguration nach ID
POST /configs Konfiguration erstellen (201)
PUT /configs/{id} Konfiguration aktualisieren
DELETE /configs/{id} Konfiguration löschen

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

Method Endpoint Beschreibung
GET /my?customerId= Eigene Tickets des Kunden
POST /{id}/cancel Kunde storniert eigenes Ticket
GET /{id}/qr QR-Code als PNG [AllowAnonymous]
GET /{id}/pdf Ticket als PDF [AllowAnonymous]

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

Method Endpoint Beschreibung
GET /catalog Verfügbare Ticket-Typen
GET /capacity?date=&articleId= Verfügbarkeit prüfen
POST /validate Checkout validieren
POST /purchase Tickets kaufen

Commands

Command Rückgabe Beschreibung
CreateTicketCommand List\<TicketResponse> Erstellt 1+ Tickets, generiert QR + Nummer, sendet Bestätigungs-Email mit PDF
CheckInTicketCommand CheckInResult Validiert QR, setzt Status auf CheckedIn, zeichnet Zeitstempel auf
CancelTicketCommand TicketResponse Storniert Ticket (nur Status=Purchased), prüft Stornofrist, sendet Stornierungs-Email
ProcessTicketPaymentCommand TicketResponse Verbucht Restzahlung, aktualisiert PaidAmount/RemainingAmount
RefundTicketCommand TicketResponse Erstattung (nur Status=Cancelled), prüft AllowRefund
CancelTicketGroupCommand List\<TicketResponse> Storniert alle Tickets einer Gruppenbestellung
ValidateTicketCheckoutCommand TicketCheckoutValidationResult Kapazitäts- und Konfigurationsprüfung für den Shop
CreateTicketArticleConfigCommand TicketArticleConfigResponse Neue Ticket-Konfiguration erstellen
UpdateTicketArticleConfigCommand TicketArticleConfigResponse Konfiguration aktualisieren
DeleteTicketArticleConfigCommand bool Konfiguration löschen

Queries

Query Rückgabe Beschreibung
GetTicketByIdQuery TicketResponse Einzelnes Ticket
GetTicketByQrCodeQuery TicketResponse Ticket per QR-Code suchen
GetTicketsByDateQuery List\<TicketResponse> Tickets nach Veranstaltungsdatum
GetTicketsByCustomerQuery List\<TicketResponse> Tickets eines Kunden
GetTicketsByOrderQuery List\<TicketResponse> Tickets einer Bestellung
SearchTicketsQuery List\<TicketResponse> Volltextsuche mit Filtern
GetTicketCapacityQuery List\<TicketCapacityResponse> Kapazität pro Artikel/Datum
GetTicketStatsQuery TicketStatsResponse Aggregierte Statistiken
ValidateTicketQuery CheckInResult Check-in-Validierung ohne Seiteneffekte
GetTicketArticleConfigsQuery List\<TicketArticleConfigResponse> Alle Konfigurationen
GetTicketArticleConfigQuery TicketArticleConfigResponse Einzelne Konfiguration
GetTicketCatalogQuery List\<TicketShopItemResponse> Online-Shop-Katalog

Services

TicketQrService (Singleton)

Generiert QR-Codes mit der QRCoder-Bibliothek.

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

TicketPdfService (Scoped)

Erstellt Ticket-PDFs im A6-Format mit QuestPDF. Enthält eingebetteten QR-Code, Ticket-Details und optionale Venue-Informationen.

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

ExpireTicketsJob (HostedService)

Background-Job, der alle 60 Minuten läuft und abgelaufene Tickets (Status=Purchased, ExpiresAt < jetzt) automatisch auf Status=Expired setzt.

DayCloseTicketStatsProvider (Scoped)

Liefert Ticket-Statistiken für den Tagesabschluss:

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

Rückgabe: SoldOnlineCount/Amount, CheckedInCount, RefundedCount/Amount

Optionale Cross-Modul-Dependency: NullDayCloseTicketStatsProvider als Default registriert, wird vom TicketingModule mit der echten Implementierung überschrieben.

Check-in-Flow

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

CheckInStatus-Enum:

Wert Bedeutung
0 - Valid Ticket gültig, Einlass erlaubt
1 - Invalid Ticket ungültig/nicht gefunden
2 - Expired Ticket abgelaufen
3 - WrongDate Falsches Veranstaltungsdatum
4 - OutsideTimeWindow Außerhalb des Einlass-Zeitfensters
5 - AlreadyUsed Bereits eingecheckt
6 - Cancelled Ticket storniert
7 - NameMismatch Namensabweichung (personalisiertes Ticket)
8 - HasRemainingAmount Offener Restbetrag (Einlass trotzdem möglich)

Online-Shop Checkout-Flow

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

Die Kapazitätsprüfung berücksichtigt MaxCapacityPerDate und OverbookingPercent:

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

Kapazitätsmanagement

Verkaufte Tickets werden in TicketArticleConfig.SoldPerDate als Dictionary gespeichert:

{
  "2026-03-15": 42,
  "2026-03-16": 85
}
  • Ohne MaxCapacityPerDate: Unbegrenzte Verfügbarkeit
  • Mit MaxCapacityPerDate: Harte Grenze pro Datum
  • Mit OverbookingPercent: Erlaubt kontrollierte Überbuchung (z.B. 10% = 110 bei Max 100)

Gruppenkauf

Tickets können als Gruppe gekauft werden:

  • AllowGroupPurchase und MaxGroupSize auf TicketArticleConfig
  • GroupOrderId verbindet alle Tickets einer Bestellung
  • GroupIndex (1-basiert) und GroupTotal für Tracking
  • CancelTicketGroupCommand storniert die gesamte Gruppe
  • Optionale GuestNames für personalisierte Gruppentickets

Disco-Integration

Ticket-Konfigurationen können mit Disco-Eintrittsgruppen verknüpft werden:

  • EnterGroupId → Automatische Zuordnung beim Ticket-Kauf
  • EnterRuleId → Spezifische Eintrittsregel
  • FreeArticles → Freiartikel aus dem Disco-Modul
  • Beim Check-in: Disco-Karte (AssignedCardId) kann zugewiesen werden

Tagesabschluss-Integration

Der IDayCloseTicketStatsProvider liefert folgende Kennzahlen für den Tagesabschluss:

Kennzahl Beschreibung
SoldOnlineCount Online verkaufte Tickets im Zeitraum
SoldOnlineAmount Gesamtumsatz online verkaufter Tickets
CheckedInCount Eingecheckte Tickets im Zeitraum
RefundedCount Erstattete Tickets im Zeitraum
RefundedAmount Gesamt-Erstattungsbetrag

Email-Integration

Zwei Email-Templates:

  • ticket_confirmation: Wird nach Ticket-Kauf gesendet, enthält PDF-Ticket als Anhang
  • ticket_cancellation: Wird nach Stornierung gesendet

OperationalConfig-Felder

Feld Typ Standard Beschreibung
TicketingEnabled bool false Ticketing-Modul aktiviert
TicketScanInDirectSale bool true POS kann Tickets scannen

Frontend-Komponenten

Admin — Tickets-Seite (/admin/tickets)

4 Tabs: 1. Tickets: Suche, Statusfilter, Datumsbereich, Tabelle mit TicketNumber/Name/EventDate/Status/Price, Storno-Aktion, PDF-Link 2. Kapazitäten: Datumsauswahl, Kapazitätskarten mit Fortschrittsbalken (Verkauft/Max/Verfügbar) 3. Statistiken: Kennzahlenkarten (Verkauft, Eingecheckt, Offen, Storniert, Abgelaufen, Erstattet, Umsatz) 4. Konfiguration: Tabelle aller Ticket-Artikel-Konfigurationen

Admin — Artikel-Editor (Ticket-Tab)

Wird angezeigt wenn ExtraOption === 15 (Ticket-Typ): - Disco-Verknüpfung (EnterGroup-Dropdown) - Extras (Freigetränke, Freiguthaben, Garderobe) - Kontingent (MaxCapacity, Overbooking) - Storno-Regeln (Frist, Erstattung) - Optionen (Personalisierung, Gruppenkauf, Zeitfenster, Teilzahlung)

POS — Ticket-Scan (/pos/ticket-scan)

  • QR-Code-Eingabefeld mit Autofocus
  • Validierungsergebnis mit farbigem Status (Grün/Orange/Rot)
  • Ticket-Details (Nummer, Name, Datum, Gast, Extras)
  • Einlass-Button bei gültigem Ticket
  • Restbetrag-Warnung bei offener Zahlung

Disco — Einlass (QR-Ticket-Sektion)

  • QR-Code-Eingabe unterhalb des Karten-Scans
  • Validierung und Check-in mit Karten-Zuweisung
  • Nur sichtbar wenn TicketingEnabled

Online-Shop — Ticket-Katalog

  • Ticket-Karten mit Preis, Perks (Freigetränke, Guthaben, Garderobe), Datum-Auswahl
  • Warenkorb-Integration mit ticket:{date} Notiz

Extension-Registrierung

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

DI-Registrierung (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(...)

Datenbank: Unterstützt CouchDB und EF Core (SQLite/SQL Server). EF Core-Konfiguration mit String-MaxLength-Constraints und Decimal-Precision(18,4). SoldPerDate wird als JSON-TEXT-Spalte gespeichert.

ID-Prefixes (IdGenerator): "Ticket" → "tkt", "TicketArticleConfig" → "tktcfg"

Abhängigkeiten

Paket Verwendung
QRCoder QR-Code-Generierung (PNG, SVG)
QuestPDF PDF-Ticket-Erstellung (A6-Format)
MediatR CQRS-Pattern
FluentValidation Request-Validierung