Initial commit - Black Canyon Tickets whitelabel platform
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1
supabase/.temp/cli-latest
Normal file
1
supabase/.temp/cli-latest
Normal file
@@ -0,0 +1 @@
|
||||
v2.30.4
|
||||
16
supabase/functions/handle-auth-signup.sql
Normal file
16
supabase/functions/handle-auth-signup.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Function to handle user signup and create user record
|
||||
CREATE OR REPLACE FUNCTION handle_auth_signup()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Create user record in users table
|
||||
INSERT INTO users (id, email, name)
|
||||
VALUES (NEW.id, NEW.email, NEW.raw_user_meta_data->>'name');
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Trigger to run the function when a user signs up
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION handle_auth_signup();
|
||||
145
supabase/migrations/001_initial_schema.sql
Normal file
145
supabase/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,145 @@
|
||||
-- Enable necessary extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Create organizations table
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
logo TEXT,
|
||||
stripe_account_id TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create users table with organization reference
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT,
|
||||
organization_id UUID REFERENCES organizations(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create events table
|
||||
CREATE TABLE events (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
venue TEXT NOT NULL,
|
||||
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
description TEXT,
|
||||
created_by UUID REFERENCES users(id) NOT NULL,
|
||||
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(slug, organization_id)
|
||||
);
|
||||
|
||||
-- Create tickets table
|
||||
CREATE TABLE tickets (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
event_id UUID REFERENCES events(id) NOT NULL,
|
||||
uuid TEXT UNIQUE NOT NULL DEFAULT uuid_generate_v4()::TEXT,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
purchaser_email TEXT NOT NULL,
|
||||
purchaser_name TEXT,
|
||||
checked_in BOOLEAN DEFAULT FALSE,
|
||||
scanned_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create payouts table
|
||||
CREATE TABLE payouts (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
event_id UUID REFERENCES events(id) NOT NULL,
|
||||
gross DECIMAL(10,2) NOT NULL,
|
||||
fee DECIMAL(10,2) NOT NULL,
|
||||
net DECIMAL(10,2) NOT NULL,
|
||||
stripe_transfer_id TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payouts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies for organizations
|
||||
CREATE POLICY "Users can view their own organization" ON organizations
|
||||
FOR SELECT USING (id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
));
|
||||
|
||||
CREATE POLICY "Users can update their own organization" ON organizations
|
||||
FOR UPDATE USING (id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
));
|
||||
|
||||
-- RLS Policies for users
|
||||
CREATE POLICY "Users can view their own profile" ON users
|
||||
FOR SELECT USING (id = auth.uid());
|
||||
|
||||
CREATE POLICY "Users can update their own profile" ON users
|
||||
FOR UPDATE USING (id = auth.uid());
|
||||
|
||||
-- RLS Policies for events
|
||||
CREATE POLICY "Users can view events from their organization" ON events
|
||||
FOR SELECT USING (organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
));
|
||||
|
||||
CREATE POLICY "Users can create events for their organization" ON events
|
||||
FOR INSERT WITH CHECK (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
) AND created_by = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can update events they created" ON events
|
||||
FOR UPDATE USING (created_by = auth.uid());
|
||||
|
||||
CREATE POLICY "Users can delete events they created" ON events
|
||||
FOR DELETE USING (created_by = auth.uid());
|
||||
|
||||
-- RLS Policies for tickets
|
||||
CREATE POLICY "Users can view tickets for their organization's events" ON tickets
|
||||
FOR SELECT USING (event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Anyone can create tickets" ON tickets
|
||||
FOR INSERT WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Users can update tickets for their organization's events" ON tickets
|
||||
FOR UPDATE USING (event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
-- RLS Policies for payouts
|
||||
CREATE POLICY "Users can view payouts for their organization's events" ON payouts
|
||||
FOR SELECT USING (event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Users can create payouts for their organization's events" ON payouts
|
||||
FOR INSERT WITH CHECK (event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_users_organization_id ON users(organization_id);
|
||||
CREATE INDEX idx_events_organization_id ON events(organization_id);
|
||||
CREATE INDEX idx_events_created_by ON events(created_by);
|
||||
CREATE INDEX idx_events_slug ON events(slug);
|
||||
CREATE INDEX idx_tickets_event_id ON tickets(event_id);
|
||||
CREATE INDEX idx_tickets_uuid ON tickets(uuid);
|
||||
CREATE INDEX idx_tickets_purchaser_email ON tickets(purchaser_email);
|
||||
CREATE INDEX idx_payouts_event_id ON payouts(event_id);
|
||||
61
supabase/migrations/002_add_fee_structure.sql
Normal file
61
supabase/migrations/002_add_fee_structure.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Add fee structure columns to organizations table
|
||||
ALTER TABLE organizations
|
||||
ADD COLUMN platform_fee_type VARCHAR(20) DEFAULT 'percentage_plus_fixed',
|
||||
ADD COLUMN platform_fee_percentage DECIMAL(5,4) DEFAULT 0.0300,
|
||||
ADD COLUMN platform_fee_fixed INTEGER DEFAULT 30,
|
||||
ADD COLUMN platform_fee_notes TEXT;
|
||||
|
||||
-- Add comments for clarity
|
||||
COMMENT ON COLUMN organizations.platform_fee_type IS 'Fee type: percentage, fixed, percentage_plus_fixed';
|
||||
COMMENT ON COLUMN organizations.platform_fee_percentage IS 'Percentage fee (0.03 = 3%)';
|
||||
COMMENT ON COLUMN organizations.platform_fee_fixed IS 'Fixed fee in cents (30 = $0.30)';
|
||||
COMMENT ON COLUMN organizations.platform_fee_notes IS 'Notes about the fee structure for this organization';
|
||||
|
||||
-- Update existing organizations with default fees
|
||||
UPDATE organizations
|
||||
SET
|
||||
platform_fee_type = 'percentage_plus_fixed',
|
||||
platform_fee_percentage = 0.0300,
|
||||
platform_fee_fixed = 30
|
||||
WHERE platform_fee_type IS NULL;
|
||||
|
||||
-- Add fee tracking to tickets table
|
||||
ALTER TABLE tickets
|
||||
ADD COLUMN platform_fee_charged INTEGER DEFAULT 0,
|
||||
ADD COLUMN organizer_net INTEGER DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN tickets.platform_fee_charged IS 'Platform fee charged in cents';
|
||||
COMMENT ON COLUMN tickets.organizer_net IS 'Net amount organizer receives in cents';
|
||||
|
||||
-- Create fee_structures table for historical tracking and templates
|
||||
CREATE TABLE fee_structures (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
fee_type VARCHAR(20) NOT NULL DEFAULT 'percentage_plus_fixed',
|
||||
fee_percentage DECIMAL(5,4) DEFAULT 0.0000,
|
||||
fee_fixed INTEGER DEFAULT 0,
|
||||
is_template BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Insert some common fee structure templates
|
||||
INSERT INTO fee_structures (name, description, fee_type, fee_percentage, fee_fixed, is_template) VALUES
|
||||
('Standard Platform Fee', 'Default 3% + $0.30 per transaction', 'percentage_plus_fixed', 0.0300, 30, true),
|
||||
('Percentage Only 3%', '3% of transaction, no fixed fee', 'percentage', 0.0300, 0, true),
|
||||
('Percentage Only 2.5%', '2.5% of transaction, no fixed fee', 'percentage', 0.0250, 0, true),
|
||||
('Fixed Fee Only', '$1.00 flat fee per transaction', 'fixed', 0.0000, 100, true),
|
||||
('Premium Rate', '3.5% + $0.50 for premium features', 'percentage_plus_fixed', 0.0350, 50, true),
|
||||
('Volume Discount', '2% + $0.25 for high-volume clients', 'percentage_plus_fixed', 0.0200, 25, true);
|
||||
|
||||
-- Enable RLS on fee_structures
|
||||
ALTER TABLE fee_structures ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Anyone can read templates
|
||||
CREATE POLICY "Anyone can view fee structure templates" ON fee_structures
|
||||
FOR SELECT USING (is_template = true);
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX idx_organizations_platform_fee_type ON organizations(platform_fee_type);
|
||||
CREATE INDEX idx_tickets_platform_fee ON tickets(platform_fee_charged);
|
||||
CREATE INDEX idx_fee_structures_template ON fee_structures(is_template);
|
||||
211
supabase/migrations/003_add_seating_and_ticket_types.sql
Normal file
211
supabase/migrations/003_add_seating_and_ticket_types.sql
Normal file
@@ -0,0 +1,211 @@
|
||||
-- Add seating and ticket type functionality
|
||||
|
||||
-- Create seating maps table
|
||||
CREATE TABLE seating_maps (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
venue_name TEXT NOT NULL,
|
||||
layout_data JSONB NOT NULL, -- Store SVG/JSON layout data
|
||||
total_capacity INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create ticket types table
|
||||
CREATE TABLE ticket_types (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
event_id UUID REFERENCES events(id) ON DELETE CASCADE NOT NULL,
|
||||
name TEXT NOT NULL, -- e.g., "General Admission", "VIP", "Balcony"
|
||||
description TEXT,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
quantity_available INTEGER, -- NULL for unlimited
|
||||
quantity_sold INTEGER DEFAULT 0,
|
||||
seating_section TEXT, -- Reference to section in seating map
|
||||
seating_map_id UUID REFERENCES seating_maps(id),
|
||||
sale_start_time TIMESTAMP WITH TIME ZONE,
|
||||
sale_end_time TIMESTAMP WITH TIME ZONE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT valid_quantities CHECK (quantity_sold <= COALESCE(quantity_available, quantity_sold))
|
||||
);
|
||||
|
||||
-- Create seats table for assigned seating
|
||||
CREATE TABLE seats (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
seating_map_id UUID REFERENCES seating_maps(id) ON DELETE CASCADE NOT NULL,
|
||||
seat_number TEXT NOT NULL, -- e.g., "A1", "B12"
|
||||
section TEXT NOT NULL, -- e.g., "Orchestra", "Balcony"
|
||||
row_name TEXT NOT NULL, -- e.g., "A", "B"
|
||||
seat_in_row INTEGER NOT NULL, -- 1, 2, 3, etc.
|
||||
x_position DECIMAL(8,3), -- X coordinate for visual positioning
|
||||
y_position DECIMAL(8,3), -- Y coordinate for visual positioning
|
||||
is_accessible BOOLEAN DEFAULT false,
|
||||
seat_type TEXT DEFAULT 'standard', -- standard, wheelchair, companion
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
UNIQUE(seating_map_id, seat_number)
|
||||
);
|
||||
|
||||
-- Update tickets table to support ticket types and seats
|
||||
ALTER TABLE tickets
|
||||
ADD COLUMN ticket_type_id UUID REFERENCES ticket_types(id),
|
||||
ADD COLUMN seat_id UUID REFERENCES seats(id),
|
||||
ADD COLUMN seat_number TEXT, -- For general admission or when seat_id is not used
|
||||
ADD COLUMN section TEXT; -- For general admission sections
|
||||
|
||||
-- Update events table to support seating maps
|
||||
ALTER TABLE events
|
||||
ADD COLUMN seating_map_id UUID REFERENCES seating_maps(id),
|
||||
ADD COLUMN seating_type VARCHAR(20) DEFAULT 'general_admission'; -- 'general_admission', 'assigned_seating', 'mixed'
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX idx_ticket_types_event_id ON ticket_types(event_id);
|
||||
CREATE INDEX idx_ticket_types_active ON ticket_types(is_active);
|
||||
CREATE INDEX idx_seats_seating_map_id ON seats(seating_map_id);
|
||||
CREATE INDEX idx_seats_section ON seats(section);
|
||||
CREATE INDEX idx_tickets_ticket_type_id ON tickets(ticket_type_id);
|
||||
CREATE INDEX idx_tickets_seat_id ON tickets(seat_id);
|
||||
|
||||
-- Enable RLS on new tables
|
||||
ALTER TABLE seating_maps ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ticket_types ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE seats ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies for seating_maps
|
||||
CREATE POLICY "Anyone can view seating maps" ON seating_maps
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "Users can create seating maps" ON seating_maps
|
||||
FOR INSERT WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Users can update their seating maps" ON seating_maps
|
||||
FOR UPDATE USING (true);
|
||||
|
||||
-- RLS Policies for ticket_types
|
||||
CREATE POLICY "Anyone can view active ticket types" ON ticket_types
|
||||
FOR SELECT USING (is_active = true OR event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Users can manage ticket types for their events" ON ticket_types
|
||||
FOR ALL USING (event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
-- RLS Policies for seats
|
||||
CREATE POLICY "Anyone can view seats" ON seats
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "Users can manage seats" ON seats
|
||||
FOR ALL USING (true);
|
||||
|
||||
-- Insert some sample seating maps
|
||||
INSERT INTO seating_maps (name, description, venue_name, layout_data, total_capacity) VALUES
|
||||
(
|
||||
'Small Theater Layout',
|
||||
'Intimate theater with 100 seats in 10 rows',
|
||||
'Black Canyon Theater',
|
||||
'{
|
||||
"type": "theater",
|
||||
"sections": [
|
||||
{
|
||||
"name": "Orchestra",
|
||||
"rows": 10,
|
||||
"seatsPerRow": 10,
|
||||
"startRow": "A",
|
||||
"pricing": "standard"
|
||||
}
|
||||
],
|
||||
"dimensions": {"width": 500, "height": 400}
|
||||
}',
|
||||
100
|
||||
),
|
||||
(
|
||||
'Concert Hall Layout',
|
||||
'Large concert hall with multiple sections',
|
||||
'Mountain View Concert Hall',
|
||||
'{
|
||||
"type": "concert_hall",
|
||||
"sections": [
|
||||
{
|
||||
"name": "Floor",
|
||||
"capacity": 500,
|
||||
"type": "general_admission"
|
||||
},
|
||||
{
|
||||
"name": "Balcony",
|
||||
"rows": 8,
|
||||
"seatsPerRow": 20,
|
||||
"startRow": "AA",
|
||||
"pricing": "premium"
|
||||
}
|
||||
],
|
||||
"dimensions": {"width": 800, "height": 600}
|
||||
}',
|
||||
660
|
||||
),
|
||||
(
|
||||
'Wedding Reception Layout',
|
||||
'Round tables for wedding reception',
|
||||
'Aspen Lodge',
|
||||
'{
|
||||
"type": "reception",
|
||||
"sections": [
|
||||
{
|
||||
"name": "Main Floor",
|
||||
"tables": 12,
|
||||
"seatsPerTable": 8,
|
||||
"tableNumbers": [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
}
|
||||
],
|
||||
"dimensions": {"width": 600, "height": 500}
|
||||
}',
|
||||
96
|
||||
);
|
||||
|
||||
-- Create function to automatically update quantity_sold when tickets are created
|
||||
CREATE OR REPLACE FUNCTION update_ticket_type_quantity()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
UPDATE ticket_types
|
||||
SET quantity_sold = quantity_sold + 1
|
||||
WHERE id = NEW.ticket_type_id;
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
UPDATE ticket_types
|
||||
SET quantity_sold = GREATEST(0, quantity_sold - 1)
|
||||
WHERE id = OLD.ticket_type_id;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create triggers for automatic quantity updates
|
||||
CREATE TRIGGER ticket_quantity_insert_trigger
|
||||
AFTER INSERT ON tickets
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.ticket_type_id IS NOT NULL)
|
||||
EXECUTE FUNCTION update_ticket_type_quantity();
|
||||
|
||||
CREATE TRIGGER ticket_quantity_delete_trigger
|
||||
AFTER DELETE ON tickets
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.ticket_type_id IS NOT NULL)
|
||||
EXECUTE FUNCTION update_ticket_type_quantity();
|
||||
|
||||
-- Add comments for clarity
|
||||
COMMENT ON TABLE seating_maps IS 'Venue seating layouts that can be reused across events';
|
||||
COMMENT ON TABLE ticket_types IS 'Different ticket categories for events (GA, VIP, etc.)';
|
||||
COMMENT ON TABLE seats IS 'Individual seats for assigned seating venues';
|
||||
COMMENT ON COLUMN ticket_types.seating_section IS 'References section name in seating_map layout_data';
|
||||
COMMENT ON COLUMN events.seating_type IS 'general_admission, assigned_seating, or mixed';
|
||||
COMMENT ON COLUMN tickets.seat_number IS 'Seat identifier for general admission or display purposes';
|
||||
COMMENT ON COLUMN tickets.section IS 'Section name for organization purposes';
|
||||
199
supabase/migrations/004_add_admin_system.sql
Normal file
199
supabase/migrations/004_add_admin_system.sql
Normal file
@@ -0,0 +1,199 @@
|
||||
-- Add admin system with role-based access control
|
||||
|
||||
-- Add user roles
|
||||
ALTER TABLE users
|
||||
ADD COLUMN role VARCHAR(20) DEFAULT 'organizer',
|
||||
ADD COLUMN is_active BOOLEAN DEFAULT true,
|
||||
ADD COLUMN last_login TIMESTAMP WITH TIME ZONE,
|
||||
ADD COLUMN created_by UUID REFERENCES users(id); -- Track who created this user
|
||||
|
||||
-- Update existing users to have organizer role
|
||||
UPDATE users SET role = 'organizer' WHERE role IS NULL;
|
||||
|
||||
-- Create admin_settings table for platform configuration
|
||||
CREATE TABLE admin_settings (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
setting_key TEXT UNIQUE NOT NULL,
|
||||
setting_value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
updated_by UUID REFERENCES users(id),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create audit_logs table for tracking admin actions
|
||||
CREATE TABLE audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
action TEXT NOT NULL, -- 'create', 'update', 'delete', 'view'
|
||||
resource_type TEXT NOT NULL, -- 'user', 'organization', 'event', 'ticket'
|
||||
resource_id UUID,
|
||||
old_values JSONB,
|
||||
new_values JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create platform_stats view for admin dashboard
|
||||
CREATE VIEW platform_stats AS
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM users WHERE role = 'organizer' AND is_active = true) as active_organizers,
|
||||
(SELECT COUNT(*) FROM users WHERE role = 'admin') as admin_users,
|
||||
(SELECT COUNT(*) FROM organizations) as total_organizations,
|
||||
(SELECT COUNT(*) FROM events) as total_events,
|
||||
(SELECT COUNT(*) FROM events WHERE start_time >= NOW()) as upcoming_events,
|
||||
(SELECT COUNT(*) FROM tickets) as total_tickets_sold,
|
||||
(SELECT COALESCE(SUM(price), 0) FROM tickets) as total_revenue,
|
||||
(SELECT COALESCE(SUM(platform_fee_charged), 0) FROM tickets) as total_platform_fees,
|
||||
(SELECT COUNT(DISTINCT DATE(created_at)) FROM tickets WHERE created_at >= NOW() - INTERVAL '30 days') as active_days_last_30,
|
||||
(SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL '7 days') as new_users_last_7_days;
|
||||
|
||||
-- Update RLS policies for admin access
|
||||
-- Users table - admins can view all users
|
||||
CREATE POLICY "Admins can view all users" ON users
|
||||
FOR SELECT USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can update any user" ON users
|
||||
FOR UPDATE USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can create users" ON users
|
||||
FOR INSERT WITH CHECK (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
|
||||
);
|
||||
|
||||
-- Organizations table - admins can view all organizations
|
||||
CREATE POLICY "Admins can view all organizations" ON organizations
|
||||
FOR SELECT USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
id IN (SELECT organization_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can update any organization" ON organizations
|
||||
FOR UPDATE USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
id IN (SELECT organization_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
|
||||
-- Events table - admins can view all events
|
||||
CREATE POLICY "Admins can view all events" ON events
|
||||
FOR SELECT USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
organization_id IN (SELECT organization_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
|
||||
-- Tickets table - admins can view all tickets
|
||||
CREATE POLICY "Admins can view all tickets" ON tickets
|
||||
FOR SELECT USING (
|
||||
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
|
||||
event_id IN (
|
||||
SELECT id FROM events WHERE organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- Enable RLS on new tables
|
||||
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE admin_settings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies for audit_logs
|
||||
CREATE POLICY "Admins can view all audit logs" ON audit_logs
|
||||
FOR SELECT USING (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
|
||||
|
||||
CREATE POLICY "System can create audit logs" ON audit_logs
|
||||
FOR INSERT WITH CHECK (true);
|
||||
|
||||
-- RLS Policies for admin_settings
|
||||
CREATE POLICY "Admins can manage settings" ON admin_settings
|
||||
FOR ALL USING (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
|
||||
|
||||
-- Insert default admin settings
|
||||
INSERT INTO admin_settings (setting_key, setting_value, description) VALUES
|
||||
('platform_name', '"Black Canyon Tickets"', 'Platform display name'),
|
||||
('platform_email', '"support@blackcanyontickets.com"', 'Platform support email'),
|
||||
('default_platform_fee_percentage', '0.03', 'Default platform fee percentage'),
|
||||
('default_platform_fee_fixed', '30', 'Default platform fee fixed amount in cents'),
|
||||
('max_events_per_organization', '100', 'Maximum events per organization'),
|
||||
('email_notifications_enabled', 'true', 'Enable email notifications'),
|
||||
('maintenance_mode', 'false', 'Platform maintenance mode');
|
||||
|
||||
-- Create function to log admin actions
|
||||
CREATE OR REPLACE FUNCTION log_admin_action(
|
||||
p_action TEXT,
|
||||
p_resource_type TEXT,
|
||||
p_resource_id UUID DEFAULT NULL,
|
||||
p_old_values JSONB DEFAULT NULL,
|
||||
p_new_values JSONB DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, old_values, new_values)
|
||||
VALUES (auth.uid(), p_action, p_resource_type, p_resource_id, p_old_values, p_new_values);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Create function to check if user is admin
|
||||
CREATE OR REPLACE FUNCTION is_admin(user_uuid UUID DEFAULT auth.uid())
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE id = user_uuid AND role = 'admin' AND is_active = true
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
CREATE INDEX idx_users_is_active ON users(is_active);
|
||||
CREATE INDEX idx_users_last_login ON users(last_login);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_logs_resource_type ON audit_logs(resource_type);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||
CREATE INDEX idx_admin_settings_key ON admin_settings(setting_key);
|
||||
|
||||
-- Update the auth signup function to handle admin creation
|
||||
CREATE OR REPLACE FUNCTION handle_auth_signup()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
user_role TEXT := 'organizer';
|
||||
BEGIN
|
||||
-- Check if this is the first user (make them admin)
|
||||
IF NOT EXISTS (SELECT 1 FROM users LIMIT 1) THEN
|
||||
user_role := 'admin';
|
||||
END IF;
|
||||
|
||||
-- Create user record in users table with error handling
|
||||
INSERT INTO users (id, email, name, role)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.email,
|
||||
COALESCE(NEW.raw_user_meta_data->>'name', NEW.email),
|
||||
user_role
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
name = COALESCE(EXCLUDED.name, users.name),
|
||||
last_login = NOW();
|
||||
|
||||
RETURN NEW;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Log the error but don't fail the auth process
|
||||
RAISE WARNING 'Failed to create user record for %: %', NEW.email, SQLERRM;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Add comments for clarity
|
||||
COMMENT ON COLUMN users.role IS 'User role: admin, organizer, staff';
|
||||
COMMENT ON COLUMN users.is_active IS 'Whether the user account is active';
|
||||
COMMENT ON COLUMN users.created_by IS 'Admin user who created this account';
|
||||
COMMENT ON TABLE audit_logs IS 'Audit trail for admin actions';
|
||||
COMMENT ON TABLE admin_settings IS 'Platform-wide configuration settings';
|
||||
COMMENT ON VIEW platform_stats IS 'Aggregated platform statistics for admin dashboard';
|
||||
184
supabase/migrations/005_add_fee_payment_model.sql
Normal file
184
supabase/migrations/005_add_fee_payment_model.sql
Normal file
@@ -0,0 +1,184 @@
|
||||
-- Add fee payment model options to organizations table
|
||||
-- This determines whether customers pay the platform fee on top of the ticket price
|
||||
-- or if the fee is absorbed into the ticket price
|
||||
|
||||
ALTER TABLE organizations
|
||||
ADD COLUMN platform_fee_model VARCHAR(20) DEFAULT 'customer_pays',
|
||||
ADD COLUMN absorb_fee_in_price BOOLEAN DEFAULT false;
|
||||
|
||||
-- Update fee_structures table to include the fee model
|
||||
ALTER TABLE fee_structures
|
||||
ADD COLUMN fee_model VARCHAR(20) DEFAULT 'customer_pays',
|
||||
ADD COLUMN absorb_fee_in_price BOOLEAN DEFAULT false;
|
||||
|
||||
-- Add comments for clarity
|
||||
COMMENT ON COLUMN organizations.platform_fee_model IS 'Fee payment model: customer_pays, absorbed_in_price';
|
||||
COMMENT ON COLUMN organizations.absorb_fee_in_price IS 'Whether to hide fee by including it in the displayed ticket price';
|
||||
COMMENT ON COLUMN fee_structures.fee_model IS 'Fee payment model: customer_pays, absorbed_in_price';
|
||||
COMMENT ON COLUMN fee_structures.absorb_fee_in_price IS 'Whether to hide fee by including it in the displayed ticket price';
|
||||
|
||||
-- Update existing organizations with default fee model and actual BCT rates
|
||||
UPDATE organizations
|
||||
SET
|
||||
platform_fee_model = 'customer_pays',
|
||||
absorb_fee_in_price = false,
|
||||
platform_fee_percentage = 0.025, -- 2.5% BCT platform fee
|
||||
platform_fee_fixed = 150 -- $1.50 BCT platform fee
|
||||
WHERE platform_fee_model IS NULL;
|
||||
|
||||
-- Update existing fee structure templates with actual rates
|
||||
UPDATE fee_structures
|
||||
SET
|
||||
fee_model = 'customer_pays',
|
||||
absorb_fee_in_price = false
|
||||
WHERE fee_model IS NULL;
|
||||
|
||||
-- Update the default template to reflect actual BCT rates
|
||||
UPDATE fee_structures
|
||||
SET
|
||||
fee_percentage = 0.025,
|
||||
fee_fixed = 150,
|
||||
description = 'BCT platform fee: 2.5% + $1.50 per transaction'
|
||||
WHERE name = 'Standard Platform Fee' AND is_template = true;
|
||||
|
||||
-- Add new fee structure templates with absorbed fee model and correct BCT rates
|
||||
INSERT INTO fee_structures (name, description, fee_type, fee_percentage, fee_fixed, fee_model, absorb_fee_in_price, is_template) VALUES
|
||||
('All-Inclusive BCT Standard', 'BCT platform fee 2.5% + $1.50 included in ticket price', 'percentage_plus_fixed', 0.025, 150, 'absorbed_in_price', true, true),
|
||||
('All-Inclusive Percentage Only', '2.5% fee included in ticket price', 'percentage', 0.025, 0, 'absorbed_in_price', true, true),
|
||||
('All-Inclusive Fixed Only', '$1.50 fee included in ticket price', 'fixed', 0.0000, 150, 'absorbed_in_price', true, true),
|
||||
('Premium All-Inclusive', 'Premium 3% + $2.00 fee included in ticket price', 'percentage_plus_fixed', 0.030, 200, 'absorbed_in_price', true, true);
|
||||
|
||||
-- Add template for Stripe fee structure (for reference/calculation)
|
||||
INSERT INTO fee_structures (name, description, fee_type, fee_percentage, fee_fixed, fee_model, absorb_fee_in_price, is_template) VALUES
|
||||
('Stripe Processing Fee', 'Stripe credit card processing: 2.99% + $0.30', 'percentage_plus_fixed', 0.0299, 30, 'customer_pays', false, true);
|
||||
|
||||
-- Add function to calculate the display price based on fee model
|
||||
CREATE OR REPLACE FUNCTION calculate_display_price(
|
||||
base_price DECIMAL,
|
||||
fee_percentage DECIMAL DEFAULT 0.03,
|
||||
fee_fixed INTEGER DEFAULT 30,
|
||||
fee_type VARCHAR DEFAULT 'percentage_plus_fixed',
|
||||
fee_model VARCHAR DEFAULT 'customer_pays'
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
platform_fee DECIMAL;
|
||||
display_price DECIMAL;
|
||||
BEGIN
|
||||
-- Calculate platform fee based on fee type
|
||||
CASE fee_type
|
||||
WHEN 'percentage' THEN
|
||||
platform_fee := base_price * fee_percentage;
|
||||
WHEN 'fixed' THEN
|
||||
platform_fee := fee_fixed / 100.0; -- Convert cents to dollars
|
||||
WHEN 'percentage_plus_fixed' THEN
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
ELSE
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
END CASE;
|
||||
|
||||
-- Calculate display price based on fee model
|
||||
CASE fee_model
|
||||
WHEN 'customer_pays' THEN
|
||||
-- Customer pays base price + platform fee
|
||||
display_price := base_price;
|
||||
WHEN 'absorbed_in_price' THEN
|
||||
-- Platform fee is absorbed into the display price
|
||||
-- To maintain the same net revenue for organizer,
|
||||
-- we need to increase the display price to cover the fee
|
||||
display_price := base_price + platform_fee;
|
||||
ELSE
|
||||
display_price := base_price;
|
||||
END CASE;
|
||||
|
||||
RETURN ROUND(display_price, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add function to calculate the total amount customer pays
|
||||
CREATE OR REPLACE FUNCTION calculate_customer_total(
|
||||
base_price DECIMAL,
|
||||
fee_percentage DECIMAL DEFAULT 0.03,
|
||||
fee_fixed INTEGER DEFAULT 30,
|
||||
fee_type VARCHAR DEFAULT 'percentage_plus_fixed',
|
||||
fee_model VARCHAR DEFAULT 'customer_pays'
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
platform_fee DECIMAL;
|
||||
customer_total DECIMAL;
|
||||
BEGIN
|
||||
-- Calculate platform fee based on fee type
|
||||
CASE fee_type
|
||||
WHEN 'percentage' THEN
|
||||
platform_fee := base_price * fee_percentage;
|
||||
WHEN 'fixed' THEN
|
||||
platform_fee := fee_fixed / 100.0; -- Convert cents to dollars
|
||||
WHEN 'percentage_plus_fixed' THEN
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
ELSE
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
END CASE;
|
||||
|
||||
-- Calculate total amount customer pays
|
||||
CASE fee_model
|
||||
WHEN 'customer_pays' THEN
|
||||
-- Customer pays base price + platform fee separately
|
||||
customer_total := base_price + platform_fee;
|
||||
WHEN 'absorbed_in_price' THEN
|
||||
-- Customer pays only the display price (fee is included)
|
||||
customer_total := base_price;
|
||||
ELSE
|
||||
customer_total := base_price + platform_fee;
|
||||
END CASE;
|
||||
|
||||
RETURN ROUND(customer_total, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add function to calculate organizer net with fee model
|
||||
CREATE OR REPLACE FUNCTION calculate_organizer_net(
|
||||
base_price DECIMAL,
|
||||
fee_percentage DECIMAL DEFAULT 0.03,
|
||||
fee_fixed INTEGER DEFAULT 30,
|
||||
fee_type VARCHAR DEFAULT 'percentage_plus_fixed',
|
||||
fee_model VARCHAR DEFAULT 'customer_pays'
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
platform_fee DECIMAL;
|
||||
organizer_net DECIMAL;
|
||||
BEGIN
|
||||
-- Calculate platform fee based on fee type
|
||||
CASE fee_type
|
||||
WHEN 'percentage' THEN
|
||||
platform_fee := base_price * fee_percentage;
|
||||
WHEN 'fixed' THEN
|
||||
platform_fee := fee_fixed / 100.0; -- Convert cents to dollars
|
||||
WHEN 'percentage_plus_fixed' THEN
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
ELSE
|
||||
platform_fee := (base_price * fee_percentage) + (fee_fixed / 100.0);
|
||||
END CASE;
|
||||
|
||||
-- Calculate organizer net (what they receive)
|
||||
organizer_net := base_price - platform_fee;
|
||||
|
||||
-- Ensure organizer net is never negative
|
||||
IF organizer_net < 0 THEN
|
||||
organizer_net := 0;
|
||||
END IF;
|
||||
|
||||
RETURN ROUND(organizer_net, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX idx_organizations_platform_fee_model ON organizations(platform_fee_model);
|
||||
CREATE INDEX idx_fee_structures_fee_model ON fee_structures(fee_model);
|
||||
|
||||
-- Add check constraints to ensure valid fee models
|
||||
ALTER TABLE organizations
|
||||
ADD CONSTRAINT check_platform_fee_model
|
||||
CHECK (platform_fee_model IN ('customer_pays', 'absorbed_in_price'));
|
||||
|
||||
ALTER TABLE fee_structures
|
||||
ADD CONSTRAINT check_fee_model
|
||||
CHECK (fee_model IN ('customer_pays', 'absorbed_in_price'));
|
||||
246
supabase/migrations/006_standardize_bct_fees.sql
Normal file
246
supabase/migrations/006_standardize_bct_fees.sql
Normal file
@@ -0,0 +1,246 @@
|
||||
-- Standardize BCT platform fees across all organizations
|
||||
-- Organizations can only customize HOW fees are applied, not the fee amounts
|
||||
|
||||
-- Remove fee amount columns from organizations table since fees are now standard
|
||||
-- Keep only the fee application model (how fees are presented to customers)
|
||||
ALTER TABLE organizations
|
||||
DROP COLUMN IF EXISTS platform_fee_type,
|
||||
DROP COLUMN IF EXISTS platform_fee_percentage,
|
||||
DROP COLUMN IF EXISTS platform_fee_fixed;
|
||||
|
||||
-- Rename fee model column for clarity
|
||||
ALTER TABLE organizations
|
||||
RENAME COLUMN platform_fee_model TO fee_display_model;
|
||||
|
||||
-- Add comment for clarity
|
||||
COMMENT ON COLUMN organizations.fee_display_model IS 'How fees are displayed to customers: customer_pays (separate line) or absorbed_in_price (included)';
|
||||
COMMENT ON COLUMN organizations.absorb_fee_in_price IS 'Whether to include BCT fee in displayed ticket price (true) or show separately (false)';
|
||||
|
||||
-- Create platform_settings table for system-wide configuration
|
||||
CREATE TABLE platform_settings (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
setting_key TEXT UNIQUE NOT NULL,
|
||||
setting_value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN DEFAULT false, -- Whether this setting can be viewed by organizers
|
||||
updated_by UUID REFERENCES users(id),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Insert standard BCT fee structure
|
||||
INSERT INTO platform_settings (setting_key, setting_value, description, is_public) VALUES
|
||||
('bct_platform_fee_percentage', '0.025', 'BCT platform fee percentage (2.5%)', true),
|
||||
('bct_platform_fee_fixed', '150', 'BCT platform fee fixed amount in cents ($1.50)', true),
|
||||
('stripe_fee_percentage', '0.0299', 'Stripe processing fee percentage (2.99%)', true),
|
||||
('stripe_fee_fixed', '30', 'Stripe processing fee fixed amount in cents ($0.30)', true),
|
||||
('platform_name', '"Black Canyon Tickets"', 'Platform display name', true),
|
||||
('platform_email', '"support@blackcanyontickets.com"', 'Platform support email', true),
|
||||
('max_events_per_organization', '100', 'Maximum events per organization', false);
|
||||
|
||||
-- Update all existing organizations to use standard fee display model
|
||||
UPDATE organizations
|
||||
SET
|
||||
fee_display_model = COALESCE(fee_display_model, 'customer_pays'),
|
||||
absorb_fee_in_price = COALESCE(absorb_fee_in_price, false);
|
||||
|
||||
-- Set default fee display model for new organizations
|
||||
ALTER TABLE organizations
|
||||
ALTER COLUMN fee_display_model SET DEFAULT 'customer_pays',
|
||||
ALTER COLUMN absorb_fee_in_price SET DEFAULT false;
|
||||
|
||||
-- Create function to get current BCT platform fees
|
||||
CREATE OR REPLACE FUNCTION get_bct_platform_fees()
|
||||
RETURNS TABLE(
|
||||
fee_percentage DECIMAL,
|
||||
fee_fixed INTEGER
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
(SELECT (setting_value#>>'{}')::DECIMAL FROM platform_settings WHERE setting_key = 'bct_platform_fee_percentage'),
|
||||
(SELECT (setting_value#>>'{}')::INTEGER FROM platform_settings WHERE setting_key = 'bct_platform_fee_fixed');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Create function to get current Stripe fees
|
||||
CREATE OR REPLACE FUNCTION get_stripe_fees()
|
||||
RETURNS TABLE(
|
||||
fee_percentage DECIMAL,
|
||||
fee_fixed INTEGER
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
(SELECT (setting_value#>>'{}')::DECIMAL FROM platform_settings WHERE setting_key = 'stripe_fee_percentage'),
|
||||
(SELECT (setting_value#>>'{}')::INTEGER FROM platform_settings WHERE setting_key = 'stripe_fee_fixed');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Update the display price calculation function to use standard fees
|
||||
CREATE OR REPLACE FUNCTION calculate_display_price_standard(
|
||||
base_price DECIMAL,
|
||||
fee_display_model VARCHAR DEFAULT 'customer_pays'
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
bct_fee_percentage DECIMAL;
|
||||
bct_fee_fixed INTEGER;
|
||||
platform_fee DECIMAL;
|
||||
display_price DECIMAL;
|
||||
BEGIN
|
||||
-- Get current BCT platform fees
|
||||
SELECT fee_percentage, fee_fixed INTO bct_fee_percentage, bct_fee_fixed
|
||||
FROM get_bct_platform_fees();
|
||||
|
||||
-- Calculate BCT platform fee
|
||||
platform_fee := (base_price * bct_fee_percentage) + (bct_fee_fixed / 100.0);
|
||||
|
||||
-- Calculate display price based on fee model
|
||||
CASE fee_display_model
|
||||
WHEN 'customer_pays' THEN
|
||||
-- Customer pays base price + platform fee separately
|
||||
display_price := base_price;
|
||||
WHEN 'absorbed_in_price' THEN
|
||||
-- Platform fee is absorbed into the display price
|
||||
display_price := base_price + platform_fee;
|
||||
ELSE
|
||||
display_price := base_price;
|
||||
END CASE;
|
||||
|
||||
RETURN ROUND(display_price, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Update the customer total calculation function to use standard fees
|
||||
CREATE OR REPLACE FUNCTION calculate_customer_total_standard(
|
||||
base_price DECIMAL,
|
||||
fee_display_model VARCHAR DEFAULT 'customer_pays'
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
bct_fee_percentage DECIMAL;
|
||||
bct_fee_fixed INTEGER;
|
||||
platform_fee DECIMAL;
|
||||
customer_total DECIMAL;
|
||||
BEGIN
|
||||
-- Get current BCT platform fees
|
||||
SELECT fee_percentage, fee_fixed INTO bct_fee_percentage, bct_fee_fixed
|
||||
FROM get_bct_platform_fees();
|
||||
|
||||
-- Calculate BCT platform fee
|
||||
platform_fee := (base_price * bct_fee_percentage) + (bct_fee_fixed / 100.0);
|
||||
|
||||
-- Calculate total amount customer pays
|
||||
CASE fee_display_model
|
||||
WHEN 'customer_pays' THEN
|
||||
-- Customer pays base price + platform fee separately
|
||||
customer_total := base_price + platform_fee;
|
||||
WHEN 'absorbed_in_price' THEN
|
||||
-- Customer pays only the display price (fee is included)
|
||||
customer_total := base_price;
|
||||
ELSE
|
||||
customer_total := base_price + platform_fee;
|
||||
END CASE;
|
||||
|
||||
RETURN ROUND(customer_total, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Update the organizer net calculation function to use standard fees
|
||||
CREATE OR REPLACE FUNCTION calculate_organizer_net_standard(
|
||||
base_price DECIMAL
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
bct_fee_percentage DECIMAL;
|
||||
bct_fee_fixed INTEGER;
|
||||
platform_fee DECIMAL;
|
||||
organizer_net DECIMAL;
|
||||
BEGIN
|
||||
-- Get current BCT platform fees
|
||||
SELECT fee_percentage, fee_fixed INTO bct_fee_percentage, bct_fee_fixed
|
||||
FROM get_bct_platform_fees();
|
||||
|
||||
-- Calculate BCT platform fee
|
||||
platform_fee := (base_price * bct_fee_percentage) + (bct_fee_fixed / 100.0);
|
||||
|
||||
-- Calculate organizer net (what they receive before Stripe fees)
|
||||
organizer_net := base_price - platform_fee;
|
||||
|
||||
-- Ensure organizer net is never negative
|
||||
IF organizer_net < 0 THEN
|
||||
organizer_net := 0;
|
||||
END IF;
|
||||
|
||||
RETURN ROUND(organizer_net, 2);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Enable RLS on platform_settings
|
||||
ALTER TABLE platform_settings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Public settings can be viewed by authenticated users
|
||||
CREATE POLICY "Authenticated users can view public platform settings" ON platform_settings
|
||||
FOR SELECT USING (is_public = true AND auth.role() = 'authenticated');
|
||||
|
||||
-- Only admins can manage platform settings
|
||||
CREATE POLICY "Admins can manage platform settings" ON platform_settings
|
||||
FOR ALL USING (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX idx_platform_settings_key ON platform_settings(setting_key);
|
||||
CREATE INDEX idx_platform_settings_public ON platform_settings(is_public);
|
||||
CREATE INDEX idx_organizations_fee_display_model ON organizations(fee_display_model);
|
||||
|
||||
-- Add check constraint to ensure valid fee display models
|
||||
ALTER TABLE organizations
|
||||
DROP CONSTRAINT IF EXISTS check_platform_fee_model,
|
||||
ADD CONSTRAINT check_fee_display_model
|
||||
CHECK (fee_display_model IN ('customer_pays', 'absorbed_in_price'));
|
||||
|
||||
-- Update fee_structures table to remove custom fee amounts (since fees are now standard)
|
||||
-- Keep only for reference and templates
|
||||
ALTER TABLE fee_structures
|
||||
ADD COLUMN is_deprecated BOOLEAN DEFAULT false;
|
||||
|
||||
-- Mark old custom fee structures as deprecated
|
||||
UPDATE fee_structures
|
||||
SET is_deprecated = true
|
||||
WHERE is_template = false;
|
||||
|
||||
-- Clean up old fee structure templates and add new standard ones
|
||||
DELETE FROM fee_structures WHERE is_template = true;
|
||||
|
||||
INSERT INTO fee_structures (name, description, fee_type, fee_percentage, fee_fixed, fee_model, absorb_fee_in_price, is_template) VALUES
|
||||
('BCT Standard - Customer Pays', 'Customer pays BCT fee (2.5% + $1.50) as separate line item', 'percentage_plus_fixed', 0.025, 150, 'customer_pays', false, true),
|
||||
('BCT Standard - All Inclusive', 'BCT fee (2.5% + $1.50) included in ticket price', 'percentage_plus_fixed', 0.025, 150, 'absorbed_in_price', true, true),
|
||||
('Stripe Processing Fee', 'Stripe credit card processing: 2.99% + $0.30', 'percentage_plus_fixed', 0.0299, 30, 'customer_pays', false, true);
|
||||
|
||||
-- Add audit logging for platform settings changes
|
||||
CREATE OR REPLACE FUNCTION log_platform_settings_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, old_values, new_values)
|
||||
VALUES (
|
||||
auth.uid(),
|
||||
'update',
|
||||
'platform_settings',
|
||||
NEW.id,
|
||||
row_to_json(OLD),
|
||||
row_to_json(NEW)
|
||||
);
|
||||
ELSIF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, new_values)
|
||||
VALUES (
|
||||
auth.uid(),
|
||||
'create',
|
||||
'platform_settings',
|
||||
NEW.id,
|
||||
row_to_json(NEW)
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER platform_settings_audit_trigger
|
||||
AFTER INSERT OR UPDATE ON platform_settings
|
||||
FOR EACH ROW EXECUTE FUNCTION log_platform_settings_change();
|
||||
271
supabase/migrations/007_add_premium_addons.sql
Normal file
271
supabase/migrations/007_add_premium_addons.sql
Normal file
@@ -0,0 +1,271 @@
|
||||
-- Create premium add-ons system for BCT platform
|
||||
-- This allows monetizing features like seating maps, AI descriptions, etc.
|
||||
|
||||
-- Create add_on_types table for available premium features
|
||||
CREATE TABLE add_on_types (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
slug TEXT UNIQUE NOT NULL, -- e.g., 'seating-maps', 'ai-description'
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
pricing_type VARCHAR(20) NOT NULL DEFAULT 'per_event', -- 'per_event', 'monthly', 'annual', 'per_ticket'
|
||||
price_cents INTEGER NOT NULL, -- Price in cents
|
||||
category VARCHAR(50) NOT NULL DEFAULT 'feature', -- 'feature', 'service', 'analytics', 'marketing'
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
requires_setup BOOLEAN DEFAULT false, -- Whether add-on needs admin setup
|
||||
auto_enable_conditions JSONB, -- Conditions for auto-enabling (e.g., event size)
|
||||
feature_flags JSONB, -- What features this unlocks
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create event_add_ons table to track purchased add-ons per event
|
||||
CREATE TABLE event_add_ons (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
event_id UUID REFERENCES events(id) ON DELETE CASCADE NOT NULL,
|
||||
add_on_type_id UUID REFERENCES add_on_types(id) NOT NULL,
|
||||
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
||||
purchase_price_cents INTEGER NOT NULL, -- Price paid (may differ from current price)
|
||||
status VARCHAR(20) DEFAULT 'active', -- 'active', 'cancelled', 'expired'
|
||||
purchased_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
expires_at TIMESTAMP WITH TIME ZONE, -- For time-limited add-ons
|
||||
metadata JSONB, -- Add-on specific configuration
|
||||
stripe_payment_intent_id TEXT, -- Link to Stripe payment
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create organization_subscriptions for monthly/annual add-ons
|
||||
CREATE TABLE organization_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
|
||||
add_on_type_id UUID REFERENCES add_on_types(id) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'active', -- 'active', 'cancelled', 'expired', 'past_due'
|
||||
current_period_start TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
current_period_end TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
stripe_subscription_id TEXT,
|
||||
stripe_customer_id TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Insert default premium add-ons
|
||||
INSERT INTO add_on_types (slug, name, description, pricing_type, price_cents, category, requires_setup, feature_flags, sort_order) VALUES
|
||||
|
||||
-- Event Setup & Management (Automated Features - Low Cost)
|
||||
('ai-event-description', 'AI Event Description', 'Professional AI-generated event descriptions optimized for your venue and audience', 'per_event', 500, 'service', false, '{"ai_description": true}', 1),
|
||||
('premium-setup-service', 'Premium Setup Service', 'Dedicated onboarding specialist helps create and optimize your event', 'per_event', 5000, 'service', true, '{"priority_support": true, "setup_assistance": true}', 2),
|
||||
('custom-event-branding', 'Custom Event Branding', 'Custom colors, styling, and logo integration for your ticket pages', 'per_event', 1000, 'feature', false, '{"custom_branding": true}', 3),
|
||||
|
||||
-- Advanced Features (Automated - Low Cost)
|
||||
('seating-maps', 'Visual Seating Management', 'Interactive venue maps with seat selection, table assignments, and VIP sections', 'per_event', 1500, 'feature', false, '{"seating_maps": true, "seat_selection": true}', 4),
|
||||
('guest-list-pro', 'Guest List Pro', 'Advanced attendee management with check-in app, VIP flagging, and notes', 'per_event', 1000, 'feature', false, '{"advanced_guest_management": true, "checkin_app": true}', 5),
|
||||
('premium-analytics', 'Premium Analytics', 'Advanced sales forecasting, customer insights, and marketing performance tracking', 'per_event', 1000, 'analytics', false, '{"advanced_analytics": true, "forecasting": true, "demographics": true}', 6),
|
||||
('ticket-scanner', 'Professional Ticket Scanner', 'Advanced QR code scanning with offline support, guest check-in tracking, and real-time reports', 'per_event', 500, 'feature', false, '{"ticket_scanner": true, "offline_scanning": true, "checkin_reports": true}', 7),
|
||||
|
||||
-- Marketing & Promotion (Mostly Automated - Moderate Cost)
|
||||
('email-marketing-suite', 'Email Marketing Suite', 'Professional email templates, automated sequences, and post-event follow-up', 'per_event', 2000, 'marketing', false, '{"email_marketing": true, "automated_sequences": true}', 8),
|
||||
('social-media-package', 'Social Media Package', 'Auto-generated posts, Instagram templates, and Facebook event integration', 'per_event', 1500, 'marketing', false, '{"social_media_tools": true, "auto_posts": true}', 9),
|
||||
|
||||
-- White-Glove Services
|
||||
('concierge-management', 'Concierge Event Management', 'Dedicated event manager with day-of coordination and real-time support', 'per_event', 50000, 'service', true, '{"dedicated_manager": true, "day_of_support": true}', 10),
|
||||
('premium-support', 'Premium Customer Support', 'Priority phone/chat support with dedicated account manager', 'per_event', 20000, 'service', false, '{"priority_support": true, "dedicated_manager": true}', 11),
|
||||
|
||||
-- Subscriptions
|
||||
('bct-pro-monthly', 'BCT Pro Monthly', 'All premium features included for unlimited events', 'monthly', 19900, 'subscription', false, '{"all_features": true, "unlimited_events": true}', 12),
|
||||
('enterprise-package', 'Enterprise Package', 'Multi-venue management, white-label options, and custom development', 'monthly', 99900, 'subscription', true, '{"multi_venue": true, "white_label": true, "api_access": true}', 13);
|
||||
|
||||
-- Function to check if organization has specific add-on for event
|
||||
CREATE OR REPLACE FUNCTION has_event_addon(p_event_id UUID, p_addon_slug TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1
|
||||
FROM event_add_ons ea
|
||||
JOIN add_on_types at ON ea.add_on_type_id = at.id
|
||||
WHERE ea.event_id = p_event_id
|
||||
AND at.slug = p_addon_slug
|
||||
AND ea.status = 'active'
|
||||
AND (ea.expires_at IS NULL OR ea.expires_at > NOW())
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to check if organization has subscription add-on
|
||||
CREATE OR REPLACE FUNCTION has_subscription_addon(p_organization_id UUID, p_addon_slug TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1
|
||||
FROM organization_subscriptions os
|
||||
JOIN add_on_types at ON os.add_on_type_id = at.id
|
||||
WHERE os.organization_id = p_organization_id
|
||||
AND at.slug = p_addon_slug
|
||||
AND os.status = 'active'
|
||||
AND os.current_period_end > NOW()
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to check if organization/event has any add-on with specific feature flag
|
||||
CREATE OR REPLACE FUNCTION has_feature_access(p_organization_id UUID, p_event_id UUID, p_feature_flag TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Check subscription add-ons
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM organization_subscriptions os
|
||||
JOIN add_on_types at ON os.add_on_type_id = at.id
|
||||
WHERE os.organization_id = p_organization_id
|
||||
AND os.status = 'active'
|
||||
AND os.current_period_end > NOW()
|
||||
AND at.feature_flags ? p_feature_flag
|
||||
) THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- Check event-specific add-ons
|
||||
IF p_event_id IS NOT NULL AND EXISTS (
|
||||
SELECT 1
|
||||
FROM event_add_ons ea
|
||||
JOIN add_on_types at ON ea.add_on_type_id = at.id
|
||||
WHERE ea.event_id = p_event_id
|
||||
AND ea.status = 'active'
|
||||
AND (ea.expires_at IS NULL OR ea.expires_at > NOW())
|
||||
AND at.feature_flags ? p_feature_flag
|
||||
) THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to get available add-ons for organization/event
|
||||
CREATE OR REPLACE FUNCTION get_available_addons(p_organization_id UUID, p_event_id UUID DEFAULT NULL)
|
||||
RETURNS TABLE(
|
||||
addon_id UUID,
|
||||
slug TEXT,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
pricing_type TEXT,
|
||||
price_cents INTEGER,
|
||||
category TEXT,
|
||||
has_access BOOLEAN,
|
||||
purchased_at TIMESTAMP WITH TIME ZONE
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
at.id as addon_id,
|
||||
at.slug,
|
||||
at.name,
|
||||
at.description,
|
||||
at.pricing_type,
|
||||
at.price_cents,
|
||||
at.category,
|
||||
CASE
|
||||
WHEN has_subscription_addon(p_organization_id, at.slug) THEN TRUE
|
||||
WHEN p_event_id IS NOT NULL AND has_event_addon(p_event_id, at.slug) THEN TRUE
|
||||
ELSE FALSE
|
||||
END as has_access,
|
||||
COALESCE(
|
||||
(SELECT ea.purchased_at FROM event_add_ons ea WHERE ea.event_id = p_event_id AND ea.add_on_type_id = at.id AND ea.status = 'active' LIMIT 1),
|
||||
(SELECT os.created_at FROM organization_subscriptions os WHERE os.organization_id = p_organization_id AND os.add_on_type_id = at.id AND os.status = 'active' LIMIT 1)
|
||||
) as purchased_at
|
||||
FROM add_on_types at
|
||||
WHERE at.is_active = true
|
||||
ORDER BY at.sort_order, at.name;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE add_on_types ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE event_add_ons ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE organization_subscriptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies for add_on_types (everyone can read active add-ons)
|
||||
CREATE POLICY "Anyone can view active add-on types" ON add_on_types
|
||||
FOR SELECT USING (is_active = true);
|
||||
|
||||
-- Admins can manage add-on types
|
||||
CREATE POLICY "Admins can manage add-on types" ON add_on_types
|
||||
FOR ALL USING (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
|
||||
|
||||
-- RLS Policies for event_add_ons
|
||||
CREATE POLICY "Users can view their organization's event add-ons" ON event_add_ons
|
||||
FOR SELECT USING (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
) OR auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can purchase add-ons for their events" ON event_add_ons
|
||||
FOR INSERT WITH CHECK (
|
||||
event_id IN (
|
||||
SELECT id FROM events WHERE created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- RLS Policies for organization_subscriptions
|
||||
CREATE POLICY "Users can view their organization's subscriptions" ON organization_subscriptions
|
||||
FOR SELECT USING (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM users WHERE id = auth.uid()
|
||||
) OR auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_add_on_types_slug ON add_on_types(slug);
|
||||
CREATE INDEX idx_add_on_types_active ON add_on_types(is_active);
|
||||
CREATE INDEX idx_add_on_types_category ON add_on_types(category);
|
||||
CREATE INDEX idx_event_add_ons_event_id ON event_add_ons(event_id);
|
||||
CREATE INDEX idx_event_add_ons_organization_id ON event_add_ons(organization_id);
|
||||
CREATE INDEX idx_event_add_ons_status ON event_add_ons(status);
|
||||
CREATE INDEX idx_organization_subscriptions_org_id ON organization_subscriptions(organization_id);
|
||||
CREATE INDEX idx_organization_subscriptions_status ON organization_subscriptions(status);
|
||||
|
||||
-- Audit logging for add-on purchases
|
||||
CREATE OR REPLACE FUNCTION log_addon_purchase()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, new_values)
|
||||
VALUES (
|
||||
auth.uid(),
|
||||
'purchase',
|
||||
'event_add_on',
|
||||
NEW.id,
|
||||
row_to_json(NEW)
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER event_add_ons_audit_trigger
|
||||
AFTER INSERT ON event_add_ons
|
||||
FOR EACH ROW EXECUTE FUNCTION log_addon_purchase();
|
||||
|
||||
-- Add add-on revenue tracking to platform_stats view
|
||||
DROP VIEW IF EXISTS platform_stats;
|
||||
CREATE VIEW platform_stats AS
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM users WHERE role = 'organizer' AND is_active = true) as active_organizers,
|
||||
(SELECT COUNT(*) FROM users WHERE role = 'admin') as admin_users,
|
||||
(SELECT COUNT(*) FROM organizations) as total_organizations,
|
||||
(SELECT COUNT(*) FROM events) as total_events,
|
||||
(SELECT COUNT(*) FROM events WHERE start_time >= NOW()) as upcoming_events,
|
||||
(SELECT COUNT(*) FROM tickets) as total_tickets_sold,
|
||||
(SELECT COALESCE(SUM(price), 0) FROM tickets) as total_revenue,
|
||||
(SELECT COALESCE(SUM(platform_fee_charged), 0) FROM tickets) as total_platform_fees,
|
||||
(SELECT COALESCE(SUM(purchase_price_cents), 0) FROM event_add_ons WHERE status = 'active') as total_addon_revenue,
|
||||
(SELECT COUNT(*) FROM event_add_ons WHERE status = 'active') as active_addons,
|
||||
(SELECT COUNT(*) FROM organization_subscriptions WHERE status = 'active') as active_subscriptions,
|
||||
(SELECT COUNT(DISTINCT DATE(created_at)) FROM tickets WHERE created_at >= NOW() - INTERVAL '30 days') as active_days_last_30,
|
||||
(SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL '7 days') as new_users_last_7_days;
|
||||
|
||||
-- Comments for documentation
|
||||
COMMENT ON TABLE add_on_types IS 'Available premium add-ons and their pricing';
|
||||
COMMENT ON TABLE event_add_ons IS 'Purchased add-ons for specific events';
|
||||
COMMENT ON TABLE organization_subscriptions IS 'Monthly/annual subscriptions for organizations';
|
||||
COMMENT ON FUNCTION has_event_addon IS 'Check if event has specific add-on purchased';
|
||||
COMMENT ON FUNCTION has_subscription_addon IS 'Check if organization has subscription add-on';
|
||||
COMMENT ON FUNCTION has_feature_access IS 'Check if organization/event has access to feature flag';
|
||||
32
supabase/migrations/008_add_featured_events_support.sql
Normal file
32
supabase/migrations/008_add_featured_events_support.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- Add fields to support featured events and public calendar integration
|
||||
ALTER TABLE events
|
||||
ADD COLUMN IF NOT EXISTS end_time TIMESTAMP WITH TIME ZONE,
|
||||
ADD COLUMN IF NOT EXISTS image_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS category TEXT DEFAULT 'general',
|
||||
ADD COLUMN IF NOT EXISTS is_featured BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS is_published BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS external_source TEXT; -- Track if event is from scraper
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_events_is_featured ON events(is_featured);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_is_public ON events(is_public);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_category ON events(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_start_time ON events(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_external_source ON events(external_source);
|
||||
|
||||
-- Add RLS policy for public events (anyone can view public events)
|
||||
CREATE POLICY IF NOT EXISTS "Anyone can view public published events" ON events
|
||||
FOR SELECT USING (is_public = true AND is_published = true);
|
||||
|
||||
-- Update existing events to be public by default for backward compatibility
|
||||
UPDATE events SET is_public = true, is_published = true WHERE is_public IS NULL;
|
||||
|
||||
-- Add comments for clarity
|
||||
COMMENT ON COLUMN events.end_time IS 'Event end time - optional, derived from start_time if not provided';
|
||||
COMMENT ON COLUMN events.image_url IS 'Featured image for the event';
|
||||
COMMENT ON COLUMN events.category IS 'Event category (music, arts, community, business, food, sports, etc.)';
|
||||
COMMENT ON COLUMN events.is_featured IS 'Whether event should be featured prominently';
|
||||
COMMENT ON COLUMN events.is_public IS 'Whether event appears in public calendar';
|
||||
COMMENT ON COLUMN events.is_published IS 'Whether event is published and visible';
|
||||
COMMENT ON COLUMN events.external_source IS 'Source of external events (e.g., "scraper", "manual")';
|
||||
157
supabase/migrations/009_add_printed_tickets.sql
Normal file
157
supabase/migrations/009_add_printed_tickets.sql
Normal file
@@ -0,0 +1,157 @@
|
||||
-- Add printed tickets support
|
||||
-- This migration adds support for printed tickets with barcodes
|
||||
|
||||
-- Create printed_tickets table
|
||||
CREATE TABLE printed_tickets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
barcode_number TEXT NOT NULL UNIQUE,
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
ticket_type_id UUID NOT NULL REFERENCES ticket_types(id) ON DELETE CASCADE,
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL DEFAULT 'valid' CHECK (status IN ('valid', 'used', 'invalid')),
|
||||
batch_number TEXT,
|
||||
notes TEXT,
|
||||
issued_by UUID REFERENCES users(id),
|
||||
checked_in_at TIMESTAMP WITH TIME ZONE,
|
||||
scanned_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create scan_attempts table for audit logging
|
||||
CREATE TABLE scan_attempts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
barcode_number TEXT NOT NULL,
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
scanned_by UUID REFERENCES users(id),
|
||||
result TEXT NOT NULL CHECK (result IN ('SUCCESS', 'INVALID_BARCODE', 'WRONG_EVENT', 'ALREADY_USED', 'NOT_VALID')),
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX idx_printed_tickets_barcode ON printed_tickets(barcode_number);
|
||||
CREATE INDEX idx_printed_tickets_event_id ON printed_tickets(event_id);
|
||||
CREATE INDEX idx_printed_tickets_org_id ON printed_tickets(organization_id);
|
||||
CREATE INDEX idx_scan_attempts_barcode ON scan_attempts(barcode_number);
|
||||
CREATE INDEX idx_scan_attempts_event_id ON scan_attempts(event_id);
|
||||
CREATE INDEX idx_scan_attempts_created_at ON scan_attempts(created_at);
|
||||
|
||||
-- Add RLS policies for multi-tenant security
|
||||
ALTER TABLE printed_tickets ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE scan_attempts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies for printed_tickets
|
||||
CREATE POLICY "Users can view printed tickets in their organization" ON printed_tickets
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = printed_tickets.organization_id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can insert printed tickets in their organization" ON printed_tickets
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = printed_tickets.organization_id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can update printed tickets in their organization" ON printed_tickets
|
||||
FOR UPDATE USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = printed_tickets.organization_id
|
||||
)
|
||||
);
|
||||
|
||||
-- Policies for scan_attempts
|
||||
CREATE POLICY "Users can view scan attempts in their organization" ON scan_attempts
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = (
|
||||
SELECT organization_id FROM events WHERE events.id = scan_attempts.event_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can insert scan attempts in their organization" ON scan_attempts
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = (
|
||||
SELECT organization_id FROM events WHERE events.id = scan_attempts.event_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- Admin override policies (for users with admin privileges)
|
||||
CREATE POLICY "Admin can view all printed tickets" ON printed_tickets
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can manage all printed tickets" ON printed_tickets
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can view all scan attempts" ON scan_attempts
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
-- Create function to automatically set organization_id when inserting printed tickets
|
||||
CREATE OR REPLACE FUNCTION set_printed_ticket_organization_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Set organization_id from the event
|
||||
NEW.organization_id = (
|
||||
SELECT organization_id
|
||||
FROM events
|
||||
WHERE id = NEW.event_id
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to automatically set organization_id
|
||||
CREATE TRIGGER set_printed_ticket_organization_id_trigger
|
||||
BEFORE INSERT ON printed_tickets
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_printed_ticket_organization_id();
|
||||
|
||||
-- Create function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_printed_ticket_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to update updated_at
|
||||
CREATE TRIGGER update_printed_ticket_updated_at_trigger
|
||||
BEFORE UPDATE ON printed_tickets
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_printed_ticket_updated_at();
|
||||
144
supabase/migrations/010_add_scanner_lock.sql
Normal file
144
supabase/migrations/010_add_scanner_lock.sql
Normal file
@@ -0,0 +1,144 @@
|
||||
-- Add scanner lock functionality to events table
|
||||
-- This migration adds support for locking scanner devices to scan-only mode
|
||||
|
||||
-- Add scanner lock fields to events table
|
||||
ALTER TABLE events ADD COLUMN scanner_lock_enabled BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE events ADD COLUMN scanner_pin_hash TEXT;
|
||||
ALTER TABLE events ADD COLUMN scanner_lock_created_at TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE events ADD COLUMN scanner_lock_created_by UUID REFERENCES users(id);
|
||||
|
||||
-- Create scanner_unlock_attempts table for audit logging
|
||||
CREATE TABLE scanner_unlock_attempts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
attempted_by UUID REFERENCES users(id),
|
||||
attempt_result TEXT NOT NULL CHECK (attempt_result IN ('SUCCESS', 'FAILED', 'INVALID_PIN')),
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
device_info TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add indexes for performance
|
||||
CREATE INDEX idx_scanner_unlock_attempts_event_id ON scanner_unlock_attempts(event_id);
|
||||
CREATE INDEX idx_scanner_unlock_attempts_created_at ON scanner_unlock_attempts(created_at);
|
||||
CREATE INDEX idx_scanner_unlock_attempts_result ON scanner_unlock_attempts(attempt_result);
|
||||
|
||||
-- Add RLS policies for scanner_unlock_attempts
|
||||
ALTER TABLE scanner_unlock_attempts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies for scanner_unlock_attempts
|
||||
CREATE POLICY "Users can view scanner unlock attempts in their organization" ON scanner_unlock_attempts
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = (
|
||||
SELECT organization_id FROM events WHERE events.id = scanner_unlock_attempts.event_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can insert scanner unlock attempts in their organization" ON scanner_unlock_attempts
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id = (
|
||||
SELECT organization_id FROM events WHERE events.id = scanner_unlock_attempts.event_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- Admin override policies (for users with admin privileges)
|
||||
CREATE POLICY "Admin can view all scanner unlock attempts" ON scanner_unlock_attempts
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can manage all scanner unlock attempts" ON scanner_unlock_attempts
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = auth.uid()
|
||||
AND users.organization_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
-- Create function to clean up scanner lock when event ends
|
||||
CREATE OR REPLACE FUNCTION cleanup_expired_scanner_locks()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
-- Disable scanner locks for events that ended more than 24 hours ago
|
||||
UPDATE events
|
||||
SET
|
||||
scanner_lock_enabled = FALSE,
|
||||
scanner_pin_hash = NULL,
|
||||
scanner_lock_created_at = NULL,
|
||||
scanner_lock_created_by = NULL
|
||||
WHERE
|
||||
scanner_lock_enabled = TRUE
|
||||
AND start_time < NOW() - INTERVAL '24 hours';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a function to be called when setting up scanner lock
|
||||
CREATE OR REPLACE FUNCTION setup_scanner_lock(
|
||||
p_event_id UUID,
|
||||
p_pin_hash TEXT
|
||||
) RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Update the event with scanner lock settings
|
||||
UPDATE events
|
||||
SET
|
||||
scanner_lock_enabled = TRUE,
|
||||
scanner_pin_hash = p_pin_hash,
|
||||
scanner_lock_created_at = NOW(),
|
||||
scanner_lock_created_by = auth.uid()
|
||||
WHERE id = p_event_id;
|
||||
|
||||
-- Return true if update was successful
|
||||
RETURN FOUND;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a function to verify scanner PIN
|
||||
CREATE OR REPLACE FUNCTION verify_scanner_pin(
|
||||
p_event_id UUID,
|
||||
p_pin_hash TEXT
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
stored_hash TEXT;
|
||||
BEGIN
|
||||
-- Get the stored PIN hash
|
||||
SELECT scanner_pin_hash INTO stored_hash
|
||||
FROM events
|
||||
WHERE id = p_event_id AND scanner_lock_enabled = TRUE;
|
||||
|
||||
-- Return true if hashes match
|
||||
RETURN stored_hash = p_pin_hash;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a function to disable scanner lock
|
||||
CREATE OR REPLACE FUNCTION disable_scanner_lock(
|
||||
p_event_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Update the event to disable scanner lock
|
||||
UPDATE events
|
||||
SET
|
||||
scanner_lock_enabled = FALSE,
|
||||
scanner_pin_hash = NULL,
|
||||
scanner_lock_created_at = NULL,
|
||||
scanner_lock_created_by = NULL
|
||||
WHERE id = p_event_id;
|
||||
|
||||
-- Return true if update was successful
|
||||
RETURN FOUND;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Reference in New Issue
Block a user