Event ticketing requires complex workflows. Seat selection, capacity management, and real-time availability challenge developers. At ZIRA Software, we've built event platforms processing thousands of tickets daily with zero overselling.
Core Features
Essential event management features:
- Event creation and scheduling
- Ticket types and pricing
- Capacity and availability tracking
- Seat selection (for venues)
- Payment processing
- Attendee management
- Check-in system
- Email confirmations
Database Schema
// Event model
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->string('venue');
$table->dateTime('starts_at');
$table->dateTime('ends_at');
$table->integer('capacity');
$table->boolean('published')->default(false);
$table->timestamps();
});
// Ticket types
Schema::create('ticket_types', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained()->onDelete('cascade');
$table->string('name'); // "General Admission", "VIP"
$table->decimal('price', 8, 2);
$table->integer('quantity');
$table->integer('quantity_sold')->default(0);
$table->dateTime('sale_starts_at')->nullable();
$table->dateTime('sale_ends_at')->nullable();
$table->timestamps();
});
// Orders
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('event_id')->constrained();
$table->string('order_number')->unique();
$table->decimal('total', 8, 2);
$table->string('status'); // pending, completed, cancelled
$table->string('payment_intent_id')->nullable();
$table->timestamps();
});
// Tickets
Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained();
$table->foreignId('ticket_type_id')->constrained();
$table->string('ticket_code')->unique();
$table->string('attendee_name');
$table->string('attendee_email');
$table->dateTime('checked_in_at')->nullable();
$table->timestamps();
});
Models and Relationships
// app/Models/Event.php
class Event extends Model
{
protected $fillable = [
'title', 'description', 'venue', 'starts_at',
'ends_at', 'capacity', 'published'
];
protected $casts = [
'starts_at' => 'datetime',
'ends_at' => 'datetime',
'published' => 'boolean',
];
public function ticketTypes()
{
return $this->hasMany(TicketType::class);
}
public function orders()
{
return $this->hasMany(Order::class);
}
public function availableTickets()
{
return $this->ticketTypes()
->where('quantity', '>', 'quantity_sold')
->where(function ($query) {
$query->whereNull('sale_starts_at')
->orWhere('sale_starts_at', '<=', now());
})
->where(function ($query) {
$query->whereNull('sale_ends_at')
->orWhere('sale_ends_at', '>=', now());
});
}
public function totalTicketsSold()
{
return $this->ticketTypes()->sum('quantity_sold');
}
public function isSoldOut()
{
return $this->totalTicketsSold() >= $this->capacity;
}
}
Ticket Purchase Flow
1. Select tickets:
// app/Http/Controllers/TicketController.php
public function select(Request $request, Event $event)
{
$validated = $request->validate([
'tickets' => 'required|array',
'tickets.*.type_id' => 'required|exists:ticket_types,id',
'tickets.*.quantity' => 'required|integer|min:1',
]);
$cart = [];
$total = 0;
foreach ($validated['tickets'] as $ticketData) {
$ticketType = TicketType::findOrFail($ticketData['type_id']);
// Check availability
if ($ticketType->quantity_sold + $ticketData['quantity'] > $ticketType->quantity) {
return back()->withErrors([
'tickets' => "Not enough {$ticketType->name} tickets available."
]);
}
$cart[] = [
'type' => $ticketType,
'quantity' => $ticketData['quantity'],
'subtotal' => $ticketType->price * $ticketData['quantity'],
];
$total += $ticketType->price * $ticketData['quantity'];
}
session(['ticket_cart' => $cart, 'cart_total' => $total]);
return redirect()->route('checkout.show', $event);
}
2. Checkout and payment:
public function checkout(Request $request, Event $event)
{
$validated = $request->validate([
'attendees' => 'required|array',
'attendees.*.name' => 'required|string|max:255',
'attendees.*.email' => 'required|email',
'payment_method' => 'required|string',
]);
DB::beginTransaction();
try {
// Create order
$order = Order::create([
'user_id' => auth()->id(),
'event_id' => $event->id,
'order_number' => $this->generateOrderNumber(),
'total' => session('cart_total'),
'status' => 'pending',
]);
// Reserve tickets (atomic operation)
foreach (session('ticket_cart') as $item) {
$ticketType = $item['type'];
// Lock row for update
$ticketType = TicketType::where('id', $ticketType->id)
->lockForUpdate()
->first();
// Check availability again
if ($ticketType->quantity_sold + $item['quantity'] > $ticketType->quantity) {
throw new \Exception('Tickets no longer available');
}
// Update sold count
$ticketType->increment('quantity_sold', $item['quantity']);
// Create individual tickets
foreach ($validated['attendees'] as $attendee) {
Ticket::create([
'order_id' => $order->id,
'ticket_type_id' => $ticketType->id,
'ticket_code' => $this->generateTicketCode(),
'attendee_name' => $attendee['name'],
'attendee_email' => $attendee['email'],
]);
}
}
// Process payment
$paymentIntent = $this->processPayment($order, $validated['payment_method']);
$order->update([
'payment_intent_id' => $paymentIntent->id,
'status' => 'completed',
]);
DB::commit();
// Send confirmation emails
event(new OrderCompleted($order));
session()->forget(['ticket_cart', 'cart_total']);
return redirect()->route('orders.show', $order);
} catch (\Exception $e) {
DB::rollBack();
return back()->withErrors(['error' => $e->getMessage()]);
}
}
QR Code Check-In
// Generate QR codes for tickets
use SimpleSoftwareIO\QrCode\Facades\QrCode;
public function generateTicketPdf(Ticket $ticket)
{
$qrCode = QrCode::size(200)->generate($ticket->ticket_code);
$pdf = PDF::loadView('tickets.pdf', compact('ticket', 'qrCode'));
return $pdf->download("ticket-{$ticket->ticket_code}.pdf");
}
// Check-in endpoint
public function checkIn(Request $request)
{
$validated = $request->validate([
'ticket_code' => 'required|string',
]);
$ticket = Ticket::where('ticket_code', $validated['ticket_code'])->first();
if (!$ticket) {
return response()->json(['error' => 'Invalid ticket code'], 404);
}
if ($ticket->checked_in_at) {
return response()->json([
'error' => 'Ticket already checked in',
'checked_in_at' => $ticket->checked_in_at,
], 400);
}
$ticket->update(['checked_in_at' => now()]);
return response()->json([
'message' => 'Check-in successful',
'attendee' => $ticket->attendee_name,
'ticket_type' => $ticket->ticketType->name,
]);
}
Seat Selection (For Venues)
// Additional schema for seated events
Schema::create('seats', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained();
$table->string('section');
$table->string('row');
$table->string('number');
$table->decimal('price', 8, 2);
$table->foreignId('ticket_id')->nullable()->constrained();
$table->timestamps();
$table->unique(['event_id', 'section', 'row', 'number']);
});
// Check seat availability
public function getAvailableSeats(Event $event)
{
return Seat::where('event_id', $event->id)
->whereNull('ticket_id')
->get()
->groupBy(['section', 'row']);
}
// Reserve seats
public function reserveSeats(array $seatIds, Order $order)
{
DB::transaction(function () use ($seatIds, $order) {
$seats = Seat::whereIn('id', $seatIds)
->whereNull('ticket_id')
->lockForUpdate()
->get();
if ($seats->count() !== count($seatIds)) {
throw new \Exception('Some seats are no longer available');
}
foreach ($seats as $seat) {
$ticket = Ticket::create([
'order_id' => $order->id,
'ticket_code' => $this->generateTicketCode(),
// ... other fields
]);
$seat->update(['ticket_id' => $ticket->id]);
}
});
}
Email Notifications
// app/Mail/TicketConfirmation.php
class TicketConfirmation extends Mailable
{
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
public function build()
{
return $this->subject('Your Tickets for ' . $this->order->event->title)
->view('emails.ticket-confirmation')
->attach($this->generateTicketsPdf());
}
}
Reporting Dashboard
public function dashboard(Event $event)
{
$metrics = [
'total_revenue' => $event->orders()
->where('status', 'completed')
->sum('total'),
'tickets_sold' => $event->totalTicketsSold(),
'capacity_percentage' => ($event->totalTicketsSold() / $event->capacity) * 100,
'checked_in' => Ticket::whereHas('order', function ($query) use ($event) {
$query->where('event_id', $event->id);
})->whereNotNull('checked_in_at')->count(),
];
$salesByType = $event->ticketTypes()
->withCount('tickets')
->get();
return view('events.dashboard', compact('event', 'metrics', 'salesByType'));
}
Conclusion
Event platforms require careful capacity management and transaction handling. Laravel's database transactions prevent overselling, while queued jobs handle email delivery.
Building an event or ticketing platform? Contact ZIRA Software for custom development.