-- Ticket system migration for match ticketing -- Migration: 20250109000001_add_ticket_system.up.sql -- Enable UUID extension if not exists CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Ticket types for different pricing tiers CREATE TABLE ticket_types ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE, name VARCHAR(255) NOT NULL, description TEXT, price_cents BIGINT NOT NULL, currency VARCHAR(10) DEFAULT 'CZK', color VARCHAR(20) DEFAULT 'primary', -- primary, secondary, success, warning, danger display_order INTEGER DEFAULT 0, active BOOLEAN DEFAULT true, -- Constraints and limits max_tickets_per_order INTEGER DEFAULT 10, sale_start_time TIMESTAMP WITH TIME ZONE, sale_end_time TIMESTAMP WITH TIME ZONE, -- Access control (optional) requires_membership BOOLEAN DEFAULT false, min_age INTEGER, INDEX idx_ticket_types_active (active), INDEX idx_ticket_types_order (display_order) ); -- Ticket campaigns for specific matches or events CREATE TABLE ticket_campaigns ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE, title VARCHAR(255) NOT NULL, description TEXT, -- Link to match (can be null for non-match events) external_match_id VARCHAR(255), -- FACR match ID competition_code VARCHAR(50), match_date_time TIMESTAMP WITH TIME ZONE, home_team VARCHAR(255), away_team VARCHAR(255), venue VARCHAR(255), -- Campaign settings active BOOLEAN DEFAULT true, sale_start_time TIMESTAMP WITH TIME ZONE NOT NULL, sale_end_time TIMESTAMP WITH TIME ZONE NOT NULL, max_total_tickets INTEGER, -- Overall capacity limit INDEX idx_ticket_campaigns_active (active), INDEX idx_ticket_campaigns_match (external_match_id), INDEX idx_ticket_campaigns_dates (sale_start_time, sale_end_time) ); -- Link ticket types to campaigns (many-to-many with campaign-specific overrides) CREATE TABLE campaign_ticket_types ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), campaign_id UUID NOT NULL REFERENCES ticket_campaigns(id) ON DELETE CASCADE, ticket_type_id UUID NOT NULL REFERENCES ticket_types(id) ON DELETE CASCADE, -- Campaign-specific overrides price_cents BIGINT, -- Override default price if set max_quantity INTEGER, -- Campaign-specific quantity limit UNIQUE(campaign_id, ticket_type_id), INDEX idx_campaign_ticket_types_campaign (campaign_id), INDEX idx_campaign_ticket_types_type (ticket_type_id) ); -- Ticket instances (actual sold tickets) CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), order_id UUID, -- Link to e-shop order when purchased campaign_id UUID NOT NULL REFERENCES ticket_campaigns(id), ticket_type_id UUID NOT NULL REFERENCES ticket_types(id), -- Ticket holder information holder_name VARCHAR(255) NOT NULL, holder_email VARCHAR(255) NOT NULL, holder_phone VARCHAR(50), -- Ticket details quantity INTEGER NOT NULL DEFAULT 1, unit_price_cents BIGINT NOT NULL, total_price_cents BIGINT NOT NULL, currency VARCHAR(10) DEFAULT 'CZK', -- Status tracking status VARCHAR(20) DEFAULT 'reserved', -- reserved, paid, cancelled, used barcode VARCHAR(255) UNIQUE, -- Unique barcode/QR code qr_code_data TEXT, -- JSON with QR code data -- Usage tracking used_at TIMESTAMP WITH TIME ZONE, used_by VARCHAR(255), -- Who validated the ticket INDEX idx_tickets_order (order_id), INDEX idx_tickets_campaign (campaign_id), INDEX idx_tickets_status (status), INDEX idx_tickets_barcode (barcode), INDEX idx_tickets_holder_email (holder_email) ); -- Ticket availability tracking per campaign/type CREATE TABLE ticket_availability ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), campaign_id UUID NOT NULL REFERENCES ticket_campaigns(id) ON DELETE CASCADE, ticket_type_id UUID NOT NULL REFERENCES ticket_types(id) ON DELETE CASCADE, total_capacity INTEGER NOT NULL DEFAULT 0, sold_quantity INTEGER NOT NULL DEFAULT 0, reserved_quantity INTEGER NOT NULL DEFAULT 0, available_quantity INTEGER GENERATED ALWAYS AS (total_capacity - sold_quantity - reserved_quantity) STORED, UNIQUE(campaign_id, ticket_type_id), INDEX idx_ticket_availability_campaign (campaign_id), INDEX idx_ticket_availability_type (ticket_type_id) ); -- Add ticket-related fields to existing e-shop orders ALTER TABLE eshop_orders ADD COLUMN ticket_order BOOLEAN DEFAULT false, ADD COLUMN ticket_campaign_id UUID REFERENCES ticket_campaigns(id); -- Create indexes for new order fields CREATE INDEX idx_eshop_orders_ticket_order ON eshop_orders(ticket_order); CREATE INDEX idx_eshop_orders_ticket_campaign ON eshop_orders(ticket_campaign_id); -- Insert default ticket types INSERT INTO ticket_types (name, description, price_cents, color, display_order) VALUES ('Dospělý', 'Vstupenka pro dospělého', 15000, 'primary', 1), ('Dítě do 15 let', 'Vstupenka pro děti do 15 let', 5000, 'success', 2), ('Student', 'Vstupenka pro studenty (platí ISIC)', 8000, 'warning', 3), ('Senior', 'Vstupenka pro seniory nad 65 let', 8000, 'secondary', 4), ('VIP', 'VIP vstupenka s občerstvením', 50000, 'danger', 5); -- Create view for available tickets with campaign info CREATE VIEW available_tickets_view AS SELECT c.id as campaign_id, c.title as campaign_title, c.description as campaign_description, c.external_match_id, c.competition_code, c.match_date_time, c.home_team, c.away_team, c.venue, c.sale_start_time, c.sale_end_time, tt.id as ticket_type_id, tt.name as ticket_type_name, tt.description as ticket_type_description, COALESCE(ctt.price_cents, tt.price_cents) as price_cents, COALESCE(ctt.max_quantity, tt.max_tickets_per_order) as max_per_order, tt.color, ta.available_quantity, ta.total_capacity, CASE WHEN NOW() < c.sale_start_time THEN 'upcoming' WHEN NOW() > c.sale_end_time THEN 'ended' WHEN ta.available_quantity <= 0 THEN 'sold_out' ELSE 'available' END as sale_status FROM ticket_campaigns c JOIN campaign_ticket_types ctt ON c.id = ctt.campaign_id JOIN ticket_types tt ON ctt.ticket_type_id = tt.id LEFT JOIN ticket_availability ta ON c.id = ta.campaign_id AND tt.id = ta.ticket_type_id WHERE c.active = true AND tt.active = true AND c.deleted_at IS NULL AND tt.deleted_at IS NULL;