Online Ticketing System¶
The ticketing system enables online sales and management of event tickets (clubs, events) with QR-code-based entry, capacity management, and integration into the end-of-day report.
Architecture¶
Ticketing is implemented as a feature pack (Dikas.Features.Ticketing), analogous to the club module. It uses the IFeatureModule interface and is integrated into the main application via 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", ID prefix tkt_, cacheAll: false
| Field | Type | Description |
|---|---|---|
| TicketNumber | string | Readable number (TKT-YYYYMMDD-XXXXXX) |
| QrCode | string | Unique QR code (GUID) |
| Name | string | Event/ticket name |
| Status | int | 0=Purchased, 1=CheckedIn, 2=Cancelled, 3=Expired, 4=Refunded |
| ArticleId | string | Reference to the ticket article |
| EventDate | DateTime | Event date |
| TimeWindowStart/End | string? | Entry time window (HH:MM) |
| ExpiresAt | DateTime | Expiry date (EventDate + 1 day) |
| EnterGroupId | string? | Club entry group |
| EnterRuleId | string? | Club entry rule |
| AssignedCardId | string? | NFC card assigned at check-in |
| FreeDrinks | int | Included free drinks |
| FreeAccount | decimal | Included credit |
| FreeArticles | List\<FreeArticleItem> | Included free articles |
| IncludesWardrobe | bool | Cloakroom included |
| GuestName | string? | Personalized guest name |
| GuestEmail | string? | Guest email |
| IsPersonalized | bool | Personalization required |
| GroupOrderId | string? | Group order ID |
| GroupIndex/GroupTotal | int? | Position/total within group |
| Price | decimal | Ticket price |
| PaidAmount | decimal | Amount already paid |
| RemainingAmount | decimal | Outstanding balance |
| RefundAmount | decimal? | Refund amount |
| RefundMethod | string? | Refund method |
| PaymentProvider | string | Payment provider (stripe, paypal, etc.) |
| PaymentTransactionId | string | External transaction ID |
| TaxClass | int | Tax class |
| CheckedInAt/By/WorkplaceId | DateTime?/string? | Check-in data |
| CancelledAt/By/CancelReason | DateTime?/string? | Cancellation data |
| RefundedAt | DateTime? | Refund timestamp |
| StatusHistory | List\<TicketStatusEntry> | Audit trail of status changes |
| OrderId | string | Shop order ID |
| CustomerId | string? | Customer reference |
TicketArticleConfig¶
BaseDocument, DocumentType "TicketArticleConfig", ID prefix tktcfg_, cacheAll: true
Configures an article as a ticket type with all defaults and rules.
| Field | Type | Description |
|---|---|---|
| ArticleId | string | Linked article |
| EnterGroupId | string? | Default club entry group |
| EnterRuleId | string? | Default club entry rule |
| FreeDrinks | int | Default free drinks |
| FreeAccount | decimal | Default free credit |
| FreeArticles | List\<FreeArticleItem> | Default free articles |
| IncludesWardrobe | bool | Cloakroom included |
| MaxCapacityPerDate | int? | Max. tickets per date |
| OverbookingPercent | int? | Overbooking in % (e.g. 10 = +10%) |
| CancelDeadlineHours | int? | Cancellation deadline in hours before event |
| AllowRefund | bool | Refund allowed |
| IsPersonalized | bool | Personalization required |
| AllowGroupPurchase | bool | Group purchase allowed |
| MaxGroupSize | int | Max. group size (default: 10) |
| TimeWindowStart/End | string? | Default entry time window |
| OnlineOnly | bool | Only available in the online shop |
| AllowPartialPayment | bool | Partial payment/deposit allowed |
| MinPrepayment | decimal | Minimum deposit |
| SoldPerDate | Dictionary\<string, int> | Tickets sold per date (yyyy-MM-dd → count) |
API Endpoints¶
TicketController — /api/v1/tickets [Authorize]¶
Tickets:
| Method | Endpoint | Description |
|---|---|---|
| GET | /{id} |
Ticket by ID |
| GET | /by-qr/{qrCode} |
Ticket by QR code |
| GET | /by-date?date= |
Tickets by date |
| GET | /by-order/{orderId} |
Tickets of an order |
| GET | /by-customer/{customerId} |
Tickets of a customer |
| GET | /search?search=&status=&dateFrom=&dateTo=&limit= |
Search tickets |
| GET | /capacity?date=&articleId= |
Query capacities |
| GET | /stats?dateFrom=&dateTo= |
Statistics |
| POST | /validate |
Validate QR code (without check-in) |
| POST | /check-in |
Perform QR entry |
| POST | /{id}/cancel |
Cancel ticket |
| POST | /{id}/pay |
Pay outstanding balance |
| POST | /{id}/refund |
Perform refund |
| POST | /cancel-group/{groupOrderId} |
Cancel group order |
Configurations:
| Method | Endpoint | Description |
|---|---|---|
| GET | /configs |
All ticket configurations |
| GET | /configs/{id} |
Configuration by ID |
| POST | /configs |
Create configuration (201) |
| PUT | /configs/{id} |
Update configuration |
| DELETE | /configs/{id} |
Delete configuration |
TicketPortalController — /api/v1/portal/tickets [Authorize(Policy="Portal")]¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /my?customerId= |
Customer's own tickets |
| POST | /{id}/cancel |
Customer cancels their own ticket |
| GET | /{id}/qr |
QR code as PNG [AllowAnonymous] |
| GET | /{id}/pdf |
Ticket as PDF [AllowAnonymous] |
TicketShopController — /api/v1/ticket-shop [AllowAnonymous]¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /catalog |
Available ticket types |
| GET | /capacity?date=&articleId= |
Check availability |
| POST | /validate |
Validate checkout |
| POST | /purchase |
Purchase tickets |
Commands¶
| Command | Return | Description |
|---|---|---|
| CreateTicketCommand | List\<TicketResponse> | Creates 1+ tickets, generates QR + number, sends confirmation email with PDF |
| CheckInTicketCommand | CheckInResult | Validates QR, sets status to CheckedIn, records timestamp |
| CancelTicketCommand | TicketResponse | Cancels ticket (only Status=Purchased), checks cancellation deadline, sends cancellation email |
| ProcessTicketPaymentCommand | TicketResponse | Books remaining payment, updates PaidAmount/RemainingAmount |
| RefundTicketCommand | TicketResponse | Refund (only Status=Cancelled), checks AllowRefund |
| CancelTicketGroupCommand | List\<TicketResponse> | Cancels all tickets of a group order |
| ValidateTicketCheckoutCommand | TicketCheckoutValidationResult | Capacity and configuration check for the shop |
| CreateTicketArticleConfigCommand | TicketArticleConfigResponse | Create new ticket configuration |
| UpdateTicketArticleConfigCommand | TicketArticleConfigResponse | Update configuration |
| DeleteTicketArticleConfigCommand | bool | Delete configuration |
Queries¶
| Query | Return | Description |
|---|---|---|
| GetTicketByIdQuery | TicketResponse | Single ticket |
| GetTicketByQrCodeQuery | TicketResponse | Find ticket by QR code |
| GetTicketsByDateQuery | List\<TicketResponse> | Tickets by event date |
| GetTicketsByCustomerQuery | List\<TicketResponse> | Tickets of a customer |
| GetTicketsByOrderQuery | List\<TicketResponse> | Tickets of an order |
| SearchTicketsQuery | List\<TicketResponse> | Full-text search with filters |
| GetTicketCapacityQuery | List\<TicketCapacityResponse> | Capacity per article/date |
| GetTicketStatsQuery | TicketStatsResponse | Aggregated statistics |
| ValidateTicketQuery | CheckInResult | Check-in validation without side effects |
| GetTicketArticleConfigsQuery | List\<TicketArticleConfigResponse> | All configurations |
| GetTicketArticleConfigQuery | TicketArticleConfigResponse | Single configuration |
| GetTicketCatalogQuery | List\<TicketShopItemResponse> | Online shop catalog |
Services¶
TicketQrService (Singleton)¶
Generates QR codes with the QRCoder library.
interface ITicketQrService {
byte[] GeneratePng(string content, int pixelsPerModule = 10);
string GenerateSvg(string content);
}
TicketPdfService (Scoped)¶
Creates ticket PDFs in A6 format with QuestPDF. Contains an embedded QR code, ticket details, and optional venue information.
ExpireTicketsJob (HostedService)¶
Background job that runs every 60 minutes and automatically sets expired tickets (Status=Purchased, ExpiresAt < now) to Status=Expired.
DayCloseTicketStatsProvider (Scoped)¶
Provides ticket statistics for the end-of-day report:
interface IDayCloseTicketStatsProvider {
Task<DayCloseTicketStats> GetStatsForPeriodAsync(DateTime from, DateTime to, CancellationToken ct);
}
Return: SoldOnlineCount/Amount, CheckedInCount, RefundedCount/Amount
Optional cross-module dependency: NullDayCloseTicketStatsProvider is registered as the default and is overridden by the TicketingModule with the real implementation.
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:
| Value | Meaning |
|---|---|
| 0 - Valid | Ticket valid, entry allowed |
| 1 - Invalid | Ticket invalid/not found |
| 2 - Expired | Ticket expired |
| 3 - WrongDate | Wrong event date |
| 4 - OutsideTimeWindow | Outside the entry time window |
| 5 - AlreadyUsed | Already checked in |
| 6 - Cancelled | Ticket cancelled |
| 7 - NameMismatch | Name discrepancy (personalized ticket) |
| 8 - HasRemainingAmount | Outstanding balance (entry still possible) |
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
The capacity check takes MaxCapacityPerDate and OverbookingPercent into account:
Effektive Kapazität = MaxCapacity + (MaxCapacity × OverbookingPercent / 100)
Verfügbar = Effektive Kapazität - SoldPerDate[datum]
Capacity Management¶
Sold tickets are stored in TicketArticleConfig.SoldPerDate as a dictionary:
- Without MaxCapacityPerDate: Unlimited availability
- With MaxCapacityPerDate: Hard limit per date
- With OverbookingPercent: Allows controlled overbooking (e.g. 10% = 110 at a max of 100)
Group Purchase¶
Tickets can be purchased as a group:
AllowGroupPurchaseandMaxGroupSizeon TicketArticleConfigGroupOrderIdlinks all tickets of an orderGroupIndex(1-based) andGroupTotalfor trackingCancelTicketGroupCommandcancels the entire group- Optional
GuestNamesfor personalized group tickets
Club Integration¶
Ticket configurations can be linked to club entry groups:
EnterGroupId→ Automatic assignment on ticket purchaseEnterRuleId→ Specific entry ruleFreeArticles→ Free articles from the club module- At check-in: club card (
AssignedCardId) can be assigned
End-of-Day Report Integration¶
The IDayCloseTicketStatsProvider provides the following metrics for the end-of-day report:
| Metric | Description |
|---|---|
| SoldOnlineCount | Tickets sold online in the period |
| SoldOnlineAmount | Total revenue of tickets sold online |
| CheckedInCount | Checked-in tickets in the period |
| RefundedCount | Refunded tickets in the period |
| RefundedAmount | Total refund amount |
Email Integration¶
Two email templates:
- ticket_confirmation: Sent after ticket purchase, contains the PDF ticket as an attachment
- ticket_cancellation: Sent after cancellation
OperationalConfig Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
| TicketingEnabled | bool | false | Ticketing module enabled |
| TicketScanInDirectSale | bool | true | POS can scan tickets |
Frontend Components¶
Admin — Tickets page (/admin/tickets)¶
4 tabs: 1. Tickets: Search, status filter, date range, table with TicketNumber/Name/EventDate/Status/Price, void action, PDF link 2. Capacities: Date selection, capacity cards with progress bars (Sold/Max/Available) 3. Statistics: Metric cards (Sold, Checked in, Outstanding, Cancelled, Expired, Refunded, Revenue) 4. Configuration: Table of all ticket article configurations
Admin — Article editor (Ticket tab)¶
Shown when ExtraOption === 15 (ticket type):
- Club link (EnterGroup dropdown)
- Extras (free drinks, free credit, cloakroom)
- Allotment (MaxCapacity, overbooking)
- Cancellation rules (deadline, refund)
- Options (personalization, group purchase, time window, partial payment)
POS — Ticket scan (/pos/ticket-scan)¶
- QR code input field with autofocus
- Validation result with colored status (Green/Orange/Red)
- Ticket details (number, name, date, guest, extras)
- Entry button for a valid ticket
- Remaining balance warning for outstanding payment
Club — Entry (QR ticket section)¶
- QR code input below the card scan
- Validation and check-in with card assignment
- Only visible when
TicketingEnabled
Online Shop — Ticket catalog¶
- Ticket cards with price, perks (free drinks, credit, cloakroom), date selection
- Cart integration with
ticket:{date}note
Extension Registration¶
// 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 Registration (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: Supports CouchDB and EF Core (SQLite/SQL Server). EF Core configuration with String MaxLength constraints and Decimal Precision(18,4). SoldPerDate is stored as a JSON TEXT column.
ID prefixes (IdGenerator): "Ticket" → "tkt", "TicketArticleConfig" → "tktcfg"
Dependencies¶
| Package | Usage |
|---|---|
| QRCoder | QR code generation (PNG, SVG) |
| QuestPDF | PDF ticket creation (A6 format) |
| MediatR | CQRS pattern |
| FluentValidation | Request validation |