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.
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:
- 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:
AllowGroupPurchaseundMaxGroupSizeauf TicketArticleConfigGroupOrderIdverbindet alle Tickets einer BestellungGroupIndex(1-basiert) undGroupTotalfür TrackingCancelTicketGroupCommandstorniert die gesamte Gruppe- Optionale
GuestNamesfür personalisierte Gruppentickets
Disco-Integration¶
Ticket-Konfigurationen können mit Disco-Eintrittsgruppen verknüpft werden:
EnterGroupId→ Automatische Zuordnung beim Ticket-KaufEnterRuleId→ Spezifische EintrittsregelFreeArticles→ 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 |