Ticketing System - Database Schema Documentation
Overview
This document provides a detailed explanation of all database tables, their columns, purposes,
and relationships in the ticketing system.
1. USERS Table
Purpose
Stores all user accounts in the system. Users can be event organizers (who create events) or
attendees (who purchase tickets), or both.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier for each user
UNIQUE, NOT NULL, User's email address for login and
email VARCHAR(255)
INDEXED communication
Bcrypt/Argon2 hashed password
password_hash VARCHAR(255) NOT NULL
(never store plain passwords)
first_name VARCHAR(100) NOT NULL User's first name
last_name VARCHAR(100) NOT NULL User's last name
User's phone number (important
phone_number VARCHAR(20) NULLABLE, INDEXED
for M-Pesa payments)
User role: 'organizer', 'attendee',
NOT NULL, DEFAULT
role ENUM 'admin'. Users can be both
'attendee'
organizer and attendee
Whether user has verified their
email_verified BOOLEAN DEFAULT false
email address
profile_image_url VARCHAR(500) NULLABLE URL to user's profile picture
DEFAULT
created_at TIMESTAMP When the account was created
CURRENT_TIMESTAMP
DEFAULT
updated_at TIMESTAMP CURRENT_TIMESTAMP ON Last time the record was modified
UPDATE
Relationships
• One-to-Many with Events: A user (as organizer) can create multiple events
• One-to-Many with Orders: A user can have multiple orders
• One-to-Many with Tickets: A user can own multiple tickets
Indexes
• Primary index on id
• Unique index on email
• Index on phone_number for quick payment lookups
• Index on role for filtering organizers
Business Logic Notes
• When a user registers, default role is 'attendee'
• Users can upgrade to 'organizer' role when they create their first event
• Email verification should be required before creating events
• Soft delete option: Add deleted_at column instead of hard deleting users
2. EVENTS Table
Purpose
Stores all events created by organizers. This is the core entity around which the entire
ticketing system revolves.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier for each event
FOREIGN KEY ([Link]), Reference to the user who created
organizer_id UUID
NOT NULL, INDEXED this event
title VARCHAR(255) NOT NULL, INDEXED Event name/title
Detailed description of the event
description TEXT NOT NULL
(supports markdown)
Event category: 'music', 'sports',
'conference', 'workshop', 'festival',
category ENUM NOT NULL
'theater', 'comedy', 'networking',
'charity', 'other'
Name of the venue (e.g., "Safari
venue_name VARCHAR(255) NOT NULL
Park Hotel")
venue_address VARCHAR(500) NOT NULL Street address of the venue
city VARCHAR(100) NOT NULL, INDEXED City where event takes place
NOT NULL, DEFAULT
country VARCHAR(100) Country where event takes place
'Kenya'
DECIMAL(10,
latitude NULLABLE Venue latitude for maps
8)
DECIMAL(11,
longitude NULLABLE Venue longitude for maps
8)
event_date_start TIMESTAMP NOT NULL, INDEXED When the event starts
event_date_end TIMESTAMP NOT NULL When the event ends
Main promotional image for the
cover_image_url VARCHAR(500) NULLABLE
event
banner_image_url VARCHAR(500) NULLABLE Wide banner image for event page
Event status: 'draft', 'published',
status ENUM NOT NULL, DEFAULT 'draft'
'cancelled', 'completed'
Whether event appears in
is_featured BOOLEAN DEFAULT false
featured section
Maximum total attendees (sum of
total_capacity INTEGER NULLABLE
all ticket types)
URL-friendly version of title (e.g.,
slug VARCHAR(255) UNIQUE, INDEXED
"christmas-concert-2024")
DEFAULT
created_at TIMESTAMP When event was created
CURRENT_TIMESTAMP
DEFAULT
updated_at TIMESTAMP CURRENT_TIMESTAMP ON Last modification time
UPDATE
Relationships
• Many-to-One with Users: Each event belongs to one organizer (user)
• One-to-Many with Ticket_Types: An event can have multiple ticket types/tiers
• One-to-Many with Orders: An event can have multiple orders
• One-to-Many with Tickets: An event generates multiple individual tickets
Indexes
• Primary index on id
• Foreign key index on organizer_id
• Index on status and event_date_start (for filtering published upcoming events)
• Index on city and category (for location/category searches)
• Index on slug for SEO-friendly URLs
Business Logic Notes
• Events must be in 'published' status to be visible to customers
• Only events with event_date_start in the future should show in main listings
• Organizers can edit events only if status is 'draft' or 'published'
• When event is cancelled, all tickets should be refunded
• Events automatically move to 'completed' status after event_date_end passes
3. TICKET_TYPES Table
Purpose
Defines different ticket tiers/types for an event (e.g., VIP, General Admission, Early Bird). This
allows flexible pricing and capacity management.
Columns
Column Data Type Constraints Purpose
Unique identifier for ticket
id UUID PRIMARY KEY
type
FOREIGN KEY ([Link]), NOT
event_id UUID Reference to parent event
NULL, INDEXED
Ticket tier name (e.g., "VIP
name VARCHAR(100) NOT NULL
Pass", "Early Bird")
What's included in this
description TEXT NULLABLE
ticket type
DECIMAL(10, Price per ticket in local
price NOT NULL
2) currency
currency VARCHAR(3) DEFAULT 'KES' Currency code (ISO 4217)
Total tickets available for
quantity_total INTEGER NOT NULL
this type
Number of tickets actually
quantity_sold INTEGER DEFAULT 0, NOT NULL
sold
Tickets currently in carts
quantity_reserved INTEGER DEFAULT 0, NOT NULL
(temporary holds)
When this ticket type goes
sale_start_date TIMESTAMP NULLABLE
on sale
When sales for this type
sale_end_date TIMESTAMP NULLABLE
end
Minimum tickets per
min_per_order INTEGER DEFAULT 1
purchase
Maximum tickets per
max_per_order INTEGER DEFAULT 10
purchase
Display order (lower
sort_order INTEGER DEFAULT 0
numbers first)
Status: 'active', 'sold_out',
status ENUM NOT NULL, DEFAULT 'active'
'inactive'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP Creation time
DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP Last modification time
ON UPDATE
Relationships
• Many-to-One with Events: Each ticket type belongs to one event
• One-to-Many with Order_Items: A ticket type can appear in multiple order line items
• One-to-Many with Tickets: Each ticket type generates multiple individual tickets
Indexes
• Primary index on id
• Foreign key index on event_id
• Composite index on ( event_id , status ) for quick availability checks
Business Logic Notes
• Available Quantity = quantity_total - quantity_sold - quantity_reserved
• Automatically set status to 'sold_out' when quantity_sold >= quantity_total
• Reserved quantities should expire after cart timeout (10-15 minutes)
• Price changes should not affect already purchased tickets
• When checking availability: quantity_sold + quantity_reserved + requested_quantity <=
quantity_total
Example Records
Event: "New Year's Eve Party"
- Ticket Type 1: "Early Bird" - KES 1,500 (100 available, sale ends Nov 30)
- Ticket Type 2: "Regular Admission" - KES 2,000 (500 available)
- Ticket Type 3: "VIP Table" - KES 10,000 (20 available, includes bottle service)
4. ORDERS Table
Purpose
Represents a purchase transaction. When a user buys tickets, an order is created containing
all items in their cart. This is the parent record for the purchase.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier for order
FOREIGN KEY ([Link]),
user_id UUID Who placed the order
NOT NULL, INDEXED
FOREIGN KEY ([Link]), Which event (for quick
event_id UUID
NOT NULL, INDEXED filtering)
Human-readable order
UNIQUE, NOT NULL,
order_number VARCHAR(50) reference (e.g., "ORD-20241103-
INDEXED
A7F9")
DECIMAL(10,
total_amount NOT NULL Total cost of all items in order
2)
currency VARCHAR(3) DEFAULT 'KES' Currency code
Order status: 'pending',
NOT NULL, DEFAULT
status ENUM 'confirmed', 'cancelled',
'pending'
'refunded', 'expired'
Payment status: 'pending',
NOT NULL, DEFAULT
payment_status ENUM 'paid', 'failed', 'refunded',
'pending'
'partially_refunded'
Method used: 'mpesa', 'card',
payment_method VARCHAR(50) NULLABLE
'bank_transfer', 'cash'
External payment reference
payment_reference VARCHAR(255) NULLABLE, INDEXED (M-Pesa transaction ID, Stripe
charge ID)
Email for order confirmation
buyer_email VARCHAR(255) NOT NULL
(may differ from user email)
Phone for M-Pesa and
buyer_phone VARCHAR(20) NOT NULL
notifications
When this pending order
expires_at TIMESTAMP NULLABLE expires (usually 15 mins after
creation)
confirmed_at TIMESTAMP NULLABLE When payment was confirmed
cancelled_at TIMESTAMP NULLABLE When order was cancelled
cancellation_reason TEXT NULLABLE Why order was cancelled
DEFAULT
created_at TIMESTAMP CURRENT_TIMESTAMP, Order creation time
INDEXED
DEFAULT
updated_at TIMESTAMP CURRENT_TIMESTAMP ON Last modification
UPDATE
Relationships
• Many-to-One with Users: Each order belongs to one user
• Many-to-One with Events: Each order is for one event (simplified model)
• One-to-Many with Order_Items: An order contains multiple line items
• One-to-Many with Tickets: An order generates multiple individual tickets
• One-to-Many with Payment_Transactions: An order may have multiple payment
attempts
Indexes
• Primary index on id
• Unique index on order_number
• Foreign key indexes on user_id , event_id
• Index on payment_reference for webhook lookups
• Composite index on ( user_id , created_at ) for user's order history
• Index on status and expires_at for cleanup jobs
Business Logic Notes
• Order Lifecycle:
1. User adds tickets to cart → Order created with status='pending'
2. Tickets are reserved (quantity_reserved incremented)
3. User initiates payment → payment_status='pending'
4. Payment succeeds → status='confirmed', payment_status='paid', tickets generated
5. If payment fails or expires → status='expired', reserved tickets released
• Orders expire after 15 minutes if unpaid
• Background job should clean up expired orders and release reservations
• Order number format: ORD-{YYYYMMDD}-{RANDOM} for easy searching
• Once order is 'confirmed', it cannot be cancelled by user (only refunded by organizer)
5. ORDER_ITEMS Table
Purpose
Stores individual line items within an order. Each row represents one ticket type and quantity
purchased.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier
FOREIGN KEY ([Link]), NOT NULL,
order_id UUID Parent order
INDEXED
FOREIGN KEY (ticket_types.id), NOT Which ticket type was
ticket_type_id UUID
NULL purchased
Number of tickets of this
quantity INTEGER NOT NULL, CHECK (quantity > 0)
type
DECIMAL(10, Price per ticket at time of
unit_price NOT NULL
2) purchase
DECIMAL(10,
subtotal NOT NULL quantity × unit_price
2)
DECIMAL(10, Any discount applied to
discount_amount DEFAULT 0
2) this line item
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP Creation time
Relationships
• Many-to-One with Orders: Each item belongs to one order
• Many-to-One with Ticket_Types: Each item references one ticket type
Indexes
• Primary index on id
• Foreign key index on order_id
• Foreign key index on ticket_type_id
Business Logic Notes
• Store unit_price at time of purchase (not a reference) because prices may change later
• subtotal should be calculated as quantity × unit_price - discount_amount
• This table acts as a snapshot of what was purchased
• Each order_item generates quantity number of individual tickets
Example
Order #ORD-20241103-A7F9 contains:
- Order_Item 1: 2 × VIP Tickets @ KES 5,000 = KES 10,000
- Order_Item 2: 4 × General Admission @ KES 2,000 = KES 8,000
Total Order Amount: KES 18,000
6. TICKETS Table
Purpose
Represents individual, unique tickets generated after successful payment. Each ticket has a
unique QR code and can be checked in independently.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique ticket identifier
FOREIGN KEY ([Link]), Which order generated this
order_id UUID
NOT NULL, INDEXED ticket
FOREIGN KEY
ticket_type_id UUID Type of ticket
(ticket_types.id), NOT NULL
attendee_name VARCHAR(200) NOT NULL Name of person attending
Email of attendee (may
attendee_email VARCHAR(255) NULLABLE
differ from buyer)
attendee_phone VARCHAR(20) NULLABLE Phone of attendee
UNIQUE, NOT NULL, Human-readable ticket ID
ticket_number VARCHAR(50)
INDEXED (e.g., "TKT-EVT123-00542")
UNIQUE, NOT NULL, Unique QR code string for
qr_code VARCHAR(255)
INDEXED check-in
URL to generated QR code
qr_code_image_url VARCHAR(500) NULLABLE
image
Status: 'valid', 'used',
status ENUM NOT NULL, DEFAULT 'valid' 'cancelled', 'transferred',
'refunded'
When ticket was scanned at
checked_in_at TIMESTAMP NULLABLE
venue
FOREIGN KEY ([Link]), Staff member who checked
checked_in_by UUID
NULLABLE in the ticket
FOREIGN KEY ([Link]), If ticket was transferred to
transferred_to_user_id UUID
NULLABLE another user
transferred_at TIMESTAMP NULLABLE When transfer occurred
DEFAULT
created_at TIMESTAMP Ticket generation time
CURRENT_TIMESTAMP
DEFAULT
updated_at TIMESTAMP CURRENT_TIMESTAMP ON Last modification
UPDATE
Relationships
• Many-to-One with Orders: Each ticket belongs to one order
• Many-to-One with Ticket_Types: Each ticket is of one type
• Many-to-One with Users (via checked_in_by): Staff member who scanned ticket
• Many-to-One with Users (via transferred_to_user_id): New owner after transfer
Indexes
• Primary index on id
• Unique index on ticket_number
• Unique index on qr_code (critical for fast check-in)
• Foreign key index on order_id
• Composite index on ( ticket_type_id , status ) for capacity reports
Business Logic Notes
• Generate QR code immediately after order confirmation
• QR code should contain: {ticket_id}:{event_id}:{secure_hash} for validation
• Once checked in (status='used'), ticket cannot be used again
• Tickets can be transferred to another user (update attendee details)
• PDF ticket should include: QR code, ticket number, event details, attendee name, terms
• Security: QR code validation should check signature to prevent forgery
QR Code Generation Example
QR Code Content:
"TKT:a7f9c2d1-...:EVT-123:SHA256HASH"
Validation:
- Verify ticket exists
- Verify not already used
- Verify belongs to correct event
- Verify hash matches
7. PAYMENT_TRANSACTIONS Table
Purpose
Logs all payment attempts and responses from payment providers. Critical for reconciliation,
debugging, and audit trails.
Columns
Column Data Type Constraints Purpose
Unique transaction
id UUID PRIMARY KEY
identifier
FOREIGN KEY ([Link]), NOT
order_id UUID Associated order
NULL, INDEXED
DECIMAL(10,
amount NOT NULL Transaction amount
2)
currency VARCHAR(3) DEFAULT 'KES' Currency code
Method: 'mpesa', 'stripe',
payment_method VARCHAR(50) NOT NULL
'paystack', 'flutterwave'
transaction_reference VARCHAR(255) NULLABLE, INDEXED Provider's transaction ID
Phone number (for M-
phone_number VARCHAR(20) NULLABLE
Pesa)
account_reference VARCHAR(100) NULLABLE M-Pesa account reference
Status: 'initiated',
status ENUM NOT NULL 'pending', 'success', 'failed',
'cancelled'
Full API response from
provider_response JSONB NULLABLE
payment provider
Error details if payment
error_message TEXT NULLABLE
failed
When webhook/callback
callback_received_at TIMESTAMP NULLABLE
was received
IP address of payment
ip_address VARCHAR(45) NULLABLE
initiator
DEFAULT
created_at TIMESTAMP CURRENT_TIMESTAMP, Transaction creation time
INDEXED
DEFAULT
updated_at TIMESTAMP CURRENT_TIMESTAMP ON Last status update
UPDATE
Relationships
• Many-to-One with Orders: Multiple payment attempts can exist for one order
Indexes
• Primary index on id
• Foreign key index on order_id
• Index on transaction_reference for webhook lookups
• Index on status and created_at for reconciliation reports
Business Logic Notes
• Payment Flow:
1. User clicks "Pay with M-Pesa" → Record created with status='initiated'
2. API call to Daraja → status='pending', store transaction_reference
3. User completes payment on phone
4. M-Pesa webhook arrives → status='success', update callback_received_at
5. Order status updated to 'confirmed'
• Keep full provider responses for debugging and compliance
• Failed payments should not delete the transaction (keep for audit)
• Retry logic: Create new transaction record for each attempt
• Use transaction_reference to prevent duplicate processing of webhooks
Example Flow
Order: ORD-20241103-A7F9 (KES 18,000)
Transaction 1:
- initiated at 10:15 AM
- status: 'initiated'
- payment_method: 'mpesa'
- phone_number: '254712345678'
Transaction 1 updated:
- transaction_reference: 'RKJ45HG3ER'
- status: 'pending'
- provider_response: { ... M-Pesa response ... }
Transaction 1 updated (callback received):
- status: 'success'
- callback_received_at: 10:17 AM
- provider_response: { ... M-Pesa confirmation ... }
Result: Order status → 'confirmed', Tickets generated
8. EMAIL_LOGS Table (Optional but Recommended)
Purpose
Tracks all emails sent by the system for debugging and ensuring delivery.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier
FOREIGN KEY ([Link]),
user_id UUID Recipient user (if applicable)
NULLABLE, INDEXED
FOREIGN KEY ([Link]),
order_id UUID Related order (if applicable)
NULLABLE, INDEXED
to_email VARCHAR(255) NOT NULL, INDEXED Recipient email address
subject VARCHAR(500) NOT NULL Email subject line
Type: 'order_confirmation',
'ticket_delivery',
email_type VARCHAR(50) NOT NULL
'password_reset',
'event_reminder', etc.
Status: 'queued', 'sent', 'failed',
status ENUM NOT NULL
'bounced'
Email provider: 'sendgrid', 'ses',
provider VARCHAR(50) NOT NULL
'smtp'
provider_message_id VARCHAR(255) NULLABLE Provider's tracking ID
sent_at TIMESTAMP NULLABLE When email was sent
error_message TEXT NULLABLE Error if sending failed
DEFAULT
created_at TIMESTAMP Creation time
CURRENT_TIMESTAMP
Relationships
• Many-to-One with Users: Email sent to a user
• Many-to-One with Orders: Email related to an order
9. EVENT_IMAGES Table (Optional)
Purpose
Stores multiple images for an event (gallery). Alternative to having just one cover image.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier
FOREIGN KEY ([Link]), NOT NULL,
event_id UUID Parent event
INDEXED
image_url VARCHAR(500) NOT NULL URL to image
Is this the main cover
is_cover BOOLEAN DEFAULT false
image?
sort_order INTEGER DEFAULT 0 Display order
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP Upload time
Relationships
• Many-to-One with Events: An event can have multiple images
10. REVIEWS Table (Optional - Future Enhancement)
Purpose
Allows attendees to review events after attendance.
Columns
Column Data Type Constraints Purpose
id UUID PRIMARY KEY Unique identifier
event_id UUID FOREIGN KEY ([Link]), NOT NULL, INDEXED Event being reviewed
user_id UUID FOREIGN KEY ([Link]), NOT NULL, INDEXED Reviewer
ticket_id UUID FOREIGN KEY ([Link]), NOT NULL Proof of attendance
rating INTEGER NOT NULL, CHECK (rating BETWEEN 1 AND 5) Star rating
comment TEXT NULLABLE Review text
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP Review date
Relationships
• Many-to-One with Events: Multiple reviews per event
• Many-to-One with Users: User can review multiple events
• One-to-One with Tickets: Each review linked to proof of attendance
Critical Relationships Summary
User → Events → Ticket Types → Orders → Order Items → Tickets
USER (Organizer)
└── creates EVENTS
├── has multiple TICKET_TYPES
│ └── defines pricing and capacity
│
└── receives ORDERS from attendees
├── contains ORDER_ITEMS (line items)
│ └── references TICKET_TYPES
│
├── generates TICKETS (individual)
│ └── each with unique QR code
│
└── has PAYMENT_TRANSACTIONS
└── tracks payment attempts
Database Constraints & Rules
Referential Integrity
• Use ON DELETE CASCADE for: order_items → orders, tickets → orders
• Use ON DELETE RESTRICT for: events → users (can't delete organizer with active events)
• Use ON DELETE SET NULL for: checked_in_by references
Check Constraints
1. ticket_types : quantity_sold + quantity_reserved <= quantity_total
2. order_items : quantity > 0
3. orders : total_amount >= 0
4. reviews : rating BETWEEN 1 AND 5
5. events : event_date_end >= event_date_start
Unique Constraints
• [Link] must be unique
• orders.order_number must be unique
• tickets.ticket_number must be unique
• tickets.qr_code must be unique
• [Link] must be unique
Indexing Strategy
High-Priority Indexes (Query Performance)
sql
-- Most frequently queried
CREATE INDEX idx_events_status_date ON events(status, event_date_start);
CREATE INDEX idx_events_city_category ON events(city, category);
CREATE INDEX idx_tickets_qr ON tickets(qr_code); -- Critical for check-in speed
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
-- For webhook lookups
CREATE INDEX idx_payment_tx_reference ON payment_transactions(transaction_reference);
Composite Indexes
sql
-- Event listing page
CREATE INDEX idx_events_published ON events(status, event_date_start)
WHERE status = 'published';
-- User's active orders
CREATE INDEX idx_orders_user_status ON orders(user_id, status, created_at DESC);
-- Ticket availability checks
CREATE INDEX idx_ticket_types_availability ON ticket_types(event_id, status);
Estimated Data Volumes (for planning)
Table Records per Event Growth Rate
Users - Grows with platform
Events 1 Depends on organizers
Ticket Types 2-5 Per event
Orders 50-1000 Per event
Order Items 50-1500 1-3 per order
Tickets 200-5000 Sum of order quantities
Payment Transactions 50-1200 1-2 per order
For 100 events with avg 500 attendees each:
• 50,000 tickets
• 15,000 orders
• 20,000 order items
• 20,000 payment transactions
Performance Considerations
Query Optimization
1. Ticket Availability: Cache in Redis with key event:{event_id}:ticket_type:{id}:available
2. Event Listings: Cache popular event queries for 5 minutes
3. User Orders: Index on (user_id, created_at DESC) for pagination
Concurrency Management
sql
-- When purchasing tickets, use row-level locking
BEGIN TRANSACTION;
SELECT quantity_sold, quantity_reserved, quantity_total
FROM ticket_types
WHERE id = ?
FOR UPDATE; -- Row lock
-- Check availability
-- Update quantity_reserved
COMMIT;
Data Archival
• Archive completed events older than 2 years to separate table
• Keep tickets and orders for legal/tax reasons (7+ years)
• Archive email logs older than 90 days
Security Considerations
1. Sensitive Data
• Never store plain text passwords (use bcrypt/argon2)
• Never log full payment details (mask phone numbers in logs)
• Encrypt provider_response in payment_transactions
2. QR Code Security
• Include cryptographic hash in QR code
• Validate hash on check-in to prevent forgery
• QR format: {ticket_id}|{event_id}|{hmac_signature}
3. Rate Limiting
• Limit check-in API: 100 requests/minute per event
• Limit ticket purchase: 10 attempts/minute per user
• Limit payment initiation: 5 attempts/15 minutes per order
4. Data Privacy (GDPR/Local Laws)
• Allow users to export their data
• Allow users to request account deletion
• Anonymize user data in old orders (keep for accounting)
Backup & Recovery Strategy
1. Daily Backups: Full database backup at 2 AM
2. Point-in-Time Recovery: Enable WAL archiving (PostgreSQL)
3. Critical Data: Replicate payment_transactions in real-time
4. Test Restores: Monthly restore tests
Next Steps for Implementation
1. Phase 1: Users, Events, Ticket_Types tables
2. Phase 2: Orders, Order_Items, Payment_Transactions
3. Phase 3: Tickets, QR code generation
4. Phase 4: Email_Logs, Reviews (optional)
Document Version: 1.0
Last Updated: November 3, 2024
Author: System Architect