feat: Complete platform enhancement with multi-tenant architecture

Major additions:
- Territory manager system with application workflow
- Custom pricing and page builder with Craft.js
- Enhanced Stripe Connect onboarding
- CodeReadr QR scanning integration
- Kiosk mode for venue sales
- Super admin dashboard and analytics
- MCP integration for AI-powered operations

Infrastructure improvements:
- Centralized API client and routing system
- Enhanced authentication with organization context
- Comprehensive theme management system
- Advanced event management with custom tabs
- Performance monitoring and accessibility features

Database schema updates:
- Territory management tables
- Custom pages and pricing structures
- Kiosk PIN system
- Enhanced organization profiles
- CodeReadr integration tables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-12 18:21:40 -06:00
parent a02d64a86c
commit 26a87d0d00
232 changed files with 33175 additions and 5365 deletions

View File

@@ -0,0 +1,125 @@
-- Add super admin support to the platform
-- This migration adds a super admin role distinction and required functions
-- Add super admin role support
ALTER TABLE users ADD COLUMN is_super_admin BOOLEAN DEFAULT false;
-- Update the first admin user to be super admin
UPDATE users SET is_super_admin = true WHERE role = 'admin' AND created_at = (
SELECT MIN(created_at) FROM users WHERE role = 'admin'
);
-- Create the make_user_admin function that's referenced in setup scripts
CREATE OR REPLACE FUNCTION make_user_admin(user_email TEXT)
RETURNS VOID AS $$
BEGIN
-- Update the user role to admin
UPDATE users
SET role = 'admin', is_active = true
WHERE email = user_email;
-- Log the admin action
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, new_values)
VALUES (
auth.uid(),
'update',
'user',
(SELECT id FROM users WHERE email = user_email),
json_build_object('role', 'admin', 'promoted_by', auth.uid())
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create function to make user super admin
CREATE OR REPLACE FUNCTION make_user_super_admin(user_email TEXT)
RETURNS VOID AS $$
BEGIN
-- Update the user role to admin and super admin
UPDATE users
SET role = 'admin', is_super_admin = true, is_active = true
WHERE email = user_email;
-- Log the super admin action
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, new_values)
VALUES (
auth.uid(),
'update',
'user',
(SELECT id FROM users WHERE email = user_email),
json_build_object('role', 'admin', 'is_super_admin', true, 'promoted_by', auth.uid())
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create function to check if user is super admin
CREATE OR REPLACE FUNCTION is_super_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_super_admin = true AND is_active = true
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Update the existing is_admin function to be more robust
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 function used by setup scripts
CREATE OR REPLACE FUNCTION exec_sql(sql_query TEXT)
RETURNS VOID AS $$
BEGIN
EXECUTE sql_query;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Add RLS policies for super admin access
-- Super admins can view all users (including other admins)
CREATE POLICY "Super admins can view all users" ON users
FOR SELECT USING (
auth.uid() IN (SELECT id FROM users WHERE role = 'admin' AND is_super_admin = true) OR
auth.uid() IN (SELECT id FROM users WHERE role = 'admin') OR
id = auth.uid()
);
-- Super admins can update any user (including other admins)
CREATE POLICY "Super admins can update any user" ON users
FOR UPDATE USING (
auth.uid() IN (SELECT id FROM users WHERE role = 'admin' AND is_super_admin = true) OR
(auth.uid() IN (SELECT id FROM users WHERE role = 'admin') AND id != auth.uid()) OR
id = auth.uid()
);
-- Update platform_stats view to include super admin info
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 users WHERE role = 'admin' AND is_super_admin = true) as super_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;
-- Add index for super admin queries
CREATE INDEX idx_users_is_super_admin ON users(is_super_admin);
-- Add comments for clarity
COMMENT ON COLUMN users.is_super_admin IS 'Whether the user has super admin privileges';
COMMENT ON FUNCTION make_user_admin(TEXT) IS 'Promotes a user to admin role';
COMMENT ON FUNCTION make_user_super_admin(TEXT) IS 'Promotes a user to super admin role';
COMMENT ON FUNCTION is_super_admin(UUID) IS 'Checks if a user is a super admin';
COMMENT ON FUNCTION exec_sql(TEXT) IS 'Executes arbitrary SQL (for setup scripts)';

View File

@@ -0,0 +1,579 @@
-- Add sample territories for testing
-- This creates realistic territory data for the Territory Manager system
-- Insert sample territories
INSERT INTO territories (id, name, boundary, population, market_size, is_active, created_at, updated_at) VALUES
(
'territory-denver-metro',
'Denver Metro Area',
'{
"type": "Polygon",
"coordinates": [[
[-105.1178, 39.7392],
[-104.8736, 39.7392],
[-104.8736, 39.9142],
[-105.1178, 39.9142],
[-105.1178, 39.7392]
]]
}',
2963821,
'large',
true,
NOW(),
NOW()
),
(
'territory-boulder-county',
'Boulder County',
'{
"type": "Polygon",
"coordinates": [[
[-105.6000, 39.9000],
[-105.0000, 39.9000],
[-105.0000, 40.3000],
[-105.6000, 40.3000],
[-105.6000, 39.9000]
]]
}',
330758,
'medium',
true,
NOW(),
NOW()
),
(
'territory-colorado-springs',
'Colorado Springs',
'{
"type": "Polygon",
"coordinates": [[
[-104.9000, 38.7000],
[-104.6000, 38.7000],
[-104.6000, 39.0000],
[-104.9000, 39.0000],
[-104.9000, 38.7000]
]]
}',
715522,
'medium',
true,
NOW(),
NOW()
),
(
'territory-fort-collins',
'Fort Collins',
'{
"type": "Polygon",
"coordinates": [[
[-105.2000, 40.4000],
[-105.0000, 40.4000],
[-105.0000, 40.7000],
[-105.2000, 40.7000],
[-105.2000, 40.4000]
]]
}',
169810,
'small',
true,
NOW(),
NOW()
),
(
'territory-grand-junction',
'Grand Junction',
'{
"type": "Polygon",
"coordinates": [[
[-108.7000, 39.0000],
[-108.4000, 39.0000],
[-108.4000, 39.2000],
[-108.7000, 39.2000],
[-108.7000, 39.0000]
]]
}',
65560,
'small',
true,
NOW(),
NOW()
),
(
'territory-aspen-vail',
'Aspen & Vail Valley',
'{
"type": "Polygon",
"coordinates": [[
[-106.9000, 39.4000],
[-106.2000, 39.4000],
[-106.2000, 39.7000],
[-106.9000, 39.7000],
[-106.9000, 39.4000]
]]
}',
25000,
'premium',
true,
NOW(),
NOW()
),
(
'territory-steamboat-springs',
'Steamboat Springs',
'{
"type": "Polygon",
"coordinates": [[
[-106.9000, 40.4000],
[-106.7000, 40.4000],
[-106.7000, 40.6000],
[-106.9000, 40.6000],
[-106.9000, 40.4000]
]]
}',
13224,
'small',
true,
NOW(),
NOW()
),
(
'territory-durango',
'Durango & Southwest Colorado',
'{
"type": "Polygon",
"coordinates": [[
[-108.0000, 37.0000],
[-107.5000, 37.0000],
[-107.5000, 37.5000],
[-108.0000, 37.5000],
[-108.0000, 37.0000]
]]
}',
52000,
'small',
true,
NOW(),
NOW()
),
(
'territory-pueblo',
'Pueblo',
'{
"type": "Polygon",
"coordinates": [[
[-104.8000, 38.1000],
[-104.4000, 38.1000],
[-104.4000, 38.4000],
[-104.8000, 38.4000],
[-104.8000, 38.1000]
]]
}',
111876,
'small',
true,
NOW(),
NOW()
),
(
'territory-greeley',
'Greeley & Weld County',
'{
"type": "Polygon",
"coordinates": [[
[-104.9000, 40.3000],
[-104.5000, 40.3000],
[-104.5000, 40.6000],
[-104.9000, 40.6000],
[-104.9000, 40.3000]
]]
}',
328981,
'medium',
true,
NOW(),
NOW()
),
(
'territory-loveland',
'Loveland',
'{
"type": "Polygon",
"coordinates": [[
[-105.2000, 40.2000],
[-105.0000, 40.2000],
[-105.0000, 40.5000],
[-105.2000, 40.5000],
[-105.2000, 40.2000]
]]
}',
76378,
'small',
true,
NOW(),
NOW()
),
(
'territory-longmont',
'Longmont',
'{
"type": "Polygon",
"coordinates": [[
[-105.2000, 40.1000],
[-105.0000, 40.1000],
[-105.0000, 40.3000],
[-105.2000, 40.3000],
[-105.2000, 40.1000]
]]
}',
98885,
'small',
true,
NOW(),
NOW()
),
(
'territory-summit-county',
'Summit County (Breckenridge, Keystone)',
'{
"type": "Polygon",
"coordinates": [[
[-106.2000, 39.4000],
[-105.9000, 39.4000],
[-105.9000, 39.7000],
[-106.2000, 39.7000],
[-106.2000, 39.4000]
]]
}',
31055,
'premium',
true,
NOW(),
NOW()
),
(
'territory-eagle-county',
'Eagle County (Vail, Beaver Creek)',
'{
"type": "Polygon",
"coordinates": [[
[-106.7000, 39.5000],
[-106.3000, 39.5000],
[-106.3000, 39.8000],
[-106.7000, 39.8000],
[-106.7000, 39.5000]
]]
}',
58731,
'premium',
true,
NOW(),
NOW()
),
(
'territory-telluride',
'Telluride',
'{
"type": "Polygon",
"coordinates": [[
[-108.0000, 37.8000],
[-107.8000, 37.8000],
[-107.8000, 38.0000],
[-108.0000, 38.0000],
[-108.0000, 37.8000]
]]
}',
2607,
'premium',
true,
NOW(),
NOW()
);
-- Add some sample territory managers (for testing purposes)
INSERT INTO territory_managers (id, user_id, territory_id, referral_code, status, application_date, approved_date, profile, earnings_data, created_at, updated_at) VALUES
(
'tm-john-denver',
'user-john-denver',
'territory-denver-metro',
'DENVER001',
'active',
NOW() - INTERVAL '30 days',
NOW() - INTERVAL '25 days',
'{
"full_name": "John Denver",
"phone": "+1-303-555-0101",
"address": {
"street": "123 Main St",
"city": "Denver",
"state": "CO",
"zip_code": "80202",
"country": "US"
},
"has_transportation": true,
"has_event_experience": true,
"motivation": "I love helping local events succeed and have extensive experience in event management."
}',
'{
"total_commission": 3250.00,
"current_month_commission": 850.00,
"total_events_referred": 12,
"success_rate": 0.85,
"average_commission_per_event": 270.83
}',
NOW() - INTERVAL '30 days',
NOW()
),
(
'tm-sarah-boulder',
'user-sarah-boulder',
'territory-boulder-county',
'BOULDER01',
'active',
NOW() - INTERVAL '45 days',
NOW() - INTERVAL '40 days',
'{
"full_name": "Sarah Mitchell",
"phone": "+1-303-555-0102",
"address": {
"street": "456 Pearl St",
"city": "Boulder",
"state": "CO",
"zip_code": "80302",
"country": "US"
},
"has_transportation": true,
"has_event_experience": true,
"motivation": "Boulder has an amazing event scene and I want to help connect organizers with the best ticketing solutions."
}',
'{
"total_commission": 2180.00,
"current_month_commission": 620.00,
"total_events_referred": 8,
"success_rate": 0.92,
"average_commission_per_event": 272.50
}',
NOW() - INTERVAL '45 days',
NOW()
),
(
'tm-mike-springs',
'user-mike-springs',
'territory-colorado-springs',
'SPRINGS01',
'active',
NOW() - INTERVAL '20 days',
NOW() - INTERVAL '15 days',
'{
"full_name": "Mike Thompson",
"phone": "+1-719-555-0103",
"address": {
"street": "789 Pikes Peak Ave",
"city": "Colorado Springs",
"state": "CO",
"zip_code": "80903",
"country": "US"
},
"has_transportation": true,
"has_event_experience": false,
"motivation": "New to events but excited to learn and help grow the Colorado Springs event community."
}',
'{
"total_commission": 480.00,
"current_month_commission": 320.00,
"total_events_referred": 3,
"success_rate": 0.75,
"average_commission_per_event": 160.00
}',
NOW() - INTERVAL '20 days',
NOW()
);
-- Add sample leads
INSERT INTO leads (id, territory_manager_id, event_name, organizer_contact, event_details, status, notes, follow_up_date, created_at, updated_at) VALUES
(
'lead-wedding-denver',
'tm-john-denver',
'Johnson Wedding Reception',
'{
"name": "Emily Johnson",
"email": "emily.johnson@email.com",
"phone": "+1-303-555-0201",
"organization": "Johnson Family",
"title": "Bride"
}',
'{
"event_type": "Wedding",
"event_date": "2025-08-15",
"venue": "Denver Art Museum",
"expected_attendance": 150,
"budget_range": "$5,000-$10,000",
"description": "Elegant wedding reception with cocktail hour and dinner"
}',
'confirmed',
'["Initial contact made", "Venue confirmed", "Date set", "Waiting for final guest count"]',
NOW() + INTERVAL '7 days',
NOW() - INTERVAL '5 days',
NOW()
),
(
'lead-festival-boulder',
'tm-sarah-boulder',
'Boulder Music Festival',
'{
"name": "David Chen",
"email": "david@bouldermusic.org",
"phone": "+1-303-555-0202",
"organization": "Boulder Music Association",
"title": "Event Director"
}',
'{
"event_type": "Music Festival",
"event_date": "2025-07-20",
"venue": "Chautauqua Park",
"expected_attendance": 500,
"budget_range": "$15,000-$25,000",
"description": "Annual outdoor music festival featuring local and regional artists"
}',
'contacted',
'["Sent initial proposal", "Discussed pricing options", "Awaiting decision"]',
NOW() + INTERVAL '3 days',
NOW() - INTERVAL '2 days',
NOW()
),
(
'lead-gala-denver',
'tm-john-denver',
'Children\'s Hospital Charity Gala',
'{
"name": "Lisa Rodriguez",
"email": "lisa.rodriguez@childrenshospital.org",
"phone": "+1-303-555-0203",
"organization": "Children\'s Hospital Colorado",
"title": "Development Manager"
}',
'{
"event_type": "Charity Gala",
"event_date": "2025-09-12",
"venue": "Union Station",
"expected_attendance": 300,
"budget_range": "$20,000-$30,000",
"description": "Annual fundraising gala with silent auction and dinner"
}',
'cold',
'["Need to follow up with initial contact"]',
NOW() + INTERVAL '1 day',
NOW() - INTERVAL '1 day',
NOW()
),
(
'lead-corporate-springs',
'tm-mike-springs',
'Tech Company Annual Conference',
'{
"name": "Robert Kim",
"email": "robert.kim@techcorp.com",
"phone": "+1-719-555-0204",
"organization": "TechCorp Solutions",
"title": "HR Director"
}',
'{
"event_type": "Corporate Conference",
"event_date": "2025-08-05",
"venue": "The Broadmoor",
"expected_attendance": 200,
"budget_range": "$10,000-$15,000",
"description": "Annual company conference with keynotes and networking"
}',
'converted',
'["Successfully converted to BCT customer", "Event setup complete", "Tickets selling well"]',
NULL,
NOW() - INTERVAL '10 days',
NOW()
);
-- Add sample commissions
INSERT INTO commissions (id, territory_manager_id, event_id, tickets_sold, commission_per_ticket, total_commission, status, payout_date, created_at, updated_at) VALUES
(
'comm-springs-tech',
'tm-mike-springs',
'event-tech-conference',
180,
0.40,
72.00,
'paid',
NOW() - INTERVAL '5 days',
NOW() - INTERVAL '10 days',
NOW()
),
(
'comm-denver-wedding',
'tm-john-denver',
'event-smith-wedding',
120,
0.40,
48.00,
'paid',
NOW() - INTERVAL '2 days',
NOW() - INTERVAL '15 days',
NOW()
),
(
'comm-boulder-concert',
'tm-sarah-boulder',
'event-boulder-concert',
250,
0.40,
100.00,
'unpaid',
NULL,
NOW() - INTERVAL '3 days',
NOW()
);
-- Add sample notifications
INSERT INTO tm_notifications (id, territory_manager_id, type, title, message, data, read, created_at) VALUES
(
'notif-john-payout',
'tm-john-denver',
'payout',
'Commission Payout Processed',
'Your commission of $48.00 for the Smith Wedding has been processed and will arrive in 2-3 business days.',
'{"amount": 48.00, "event_name": "Smith Wedding"}',
false,
NOW() - INTERVAL '2 days'
),
(
'notif-sarah-lead',
'tm-sarah-boulder',
'lead_update',
'Lead Status Updated',
'Your lead for Boulder Music Festival has been updated to "contacted" status.',
'{"lead_id": "lead-festival-boulder", "status": "contacted"}',
false,
NOW() - INTERVAL '1 day'
),
(
'notif-mike-achievement',
'tm-mike-springs',
'achievement',
'Achievement Unlocked: First Conversion!',
'Congratulations! You\'ve successfully converted your first lead. You\'ve earned the "First Conversion" achievement and a $50 bonus.',
'{"achievement_name": "First Conversion", "bonus": 50.00}',
false,
NOW() - INTERVAL '3 days'
),
(
'notif-john-system',
'tm-john-denver',
'system',
'New Marketing Materials Available',
'We\'ve added new marketing materials to your toolkit including updated flyers and email templates.',
'{"materials_count": 3}',
true,
NOW() - INTERVAL '5 days'
);
-- Update territories with assignments
UPDATE territories SET assigned_to = 'tm-john-denver' WHERE id = 'territory-denver-metro';
UPDATE territories SET assigned_to = 'tm-sarah-boulder' WHERE id = 'territory-boulder-county';
UPDATE territories SET assigned_to = 'tm-mike-springs' WHERE id = 'territory-colorado-springs';

View File

@@ -0,0 +1,186 @@
-- CodeREADr Integration Tables
-- Table to store CodeREADr configuration for each event
CREATE TABLE codereadr_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- CodeREADr API entities
database_id TEXT NOT NULL,
service_id TEXT NOT NULL,
user_id TEXT NOT NULL,
-- Readable names for reference
database_name TEXT NOT NULL,
service_name TEXT NOT NULL,
user_name TEXT NOT NULL,
-- Status and metadata
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'error')),
last_sync_at TIMESTAMP WITH TIME ZONE,
total_scans INTEGER DEFAULT 0,
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
-- Unique constraint to prevent multiple configs per event
UNIQUE(event_id, organization_id)
);
-- Table to store synchronized scans from CodeREADr
CREATE TABLE codereadr_scans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
codereadr_scan_id TEXT NOT NULL UNIQUE, -- CodeREADr's scan ID
-- Event and service references
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
service_id TEXT NOT NULL,
-- Scan data
ticket_uuid TEXT NOT NULL,
scan_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
response TEXT, -- CodeREADr response data
device_id TEXT,
location JSONB, -- {latitude, longitude} if available
-- Sync metadata
synced_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
webhook_processed BOOLEAN DEFAULT false,
ticket_updated BOOLEAN DEFAULT false,
error_message TEXT,
-- Index for quick lookups
INDEX idx_codereadr_scans_event_id (event_id),
INDEX idx_codereadr_scans_ticket_uuid (ticket_uuid),
INDEX idx_codereadr_scans_timestamp (scan_timestamp),
INDEX idx_codereadr_scans_service_id (service_id)
);
-- Add scan_method column to existing tickets table to track how ticket was scanned
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS scan_method TEXT DEFAULT 'manual' CHECK (scan_method IN ('manual', 'qr', 'codereadr', 'api'));
-- Add scan_method column to existing scan_attempts table
ALTER TABLE scan_attempts ADD COLUMN IF NOT EXISTS scan_method TEXT DEFAULT 'manual' CHECK (scan_method IN ('manual', 'qr', 'codereadr', 'api'));
-- Update existing scan attempts to have a scan_method
UPDATE scan_attempts SET scan_method = 'qr' WHERE scan_method IS NULL;
UPDATE tickets SET scan_method = 'qr' WHERE scan_method IS NULL AND checked_in = true;
-- Add RLS policies for codereadr_configs table
ALTER TABLE codereadr_configs ENABLE ROW LEVEL SECURITY;
-- Users can only access configs for their organization
CREATE POLICY "Users can view codereadr_configs for their organization" ON codereadr_configs
FOR SELECT USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
CREATE POLICY "Users can insert codereadr_configs for their organization" ON codereadr_configs
FOR INSERT WITH CHECK (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
CREATE POLICY "Users can update codereadr_configs for their organization" ON codereadr_configs
FOR UPDATE USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
CREATE POLICY "Users can delete codereadr_configs for their organization" ON codereadr_configs
FOR DELETE USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
-- Admin bypass for codereadr_configs
CREATE POLICY "Admins can manage all codereadr_configs" ON codereadr_configs
FOR ALL USING (
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid()
AND role = 'admin'
)
);
-- Add RLS policies for codereadr_scans table
ALTER TABLE codereadr_scans ENABLE ROW LEVEL SECURITY;
-- Users can only access scans for events in their organization
CREATE POLICY "Users can view codereadr_scans for their organization" ON codereadr_scans
FOR SELECT USING (
event_id IN (
SELECT e.id FROM events e
JOIN users u ON e.organization_id = u.organization_id
WHERE u.id = auth.uid()
)
);
CREATE POLICY "Users can insert codereadr_scans for their organization" ON codereadr_scans
FOR INSERT WITH CHECK (
event_id IN (
SELECT e.id FROM events e
JOIN users u ON e.organization_id = u.organization_id
WHERE u.id = auth.uid()
)
);
-- Admin bypass for codereadr_scans
CREATE POLICY "Admins can manage all codereadr_scans" ON codereadr_scans
FOR ALL USING (
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid()
AND role = 'admin'
)
);
-- Add indexes for better performance
CREATE INDEX IF NOT EXISTS idx_codereadr_configs_event_id ON codereadr_configs(event_id);
CREATE INDEX IF NOT EXISTS idx_codereadr_configs_organization_id ON codereadr_configs(organization_id);
CREATE INDEX IF NOT EXISTS idx_codereadr_configs_status ON codereadr_configs(status);
-- Add function to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Add trigger for codereadr_configs updated_at
CREATE TRIGGER update_codereadr_configs_updated_at
BEFORE UPDATE ON codereadr_configs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Add useful views for reporting
CREATE VIEW codereadr_scan_summary AS
SELECT
c.event_id,
c.organization_id,
c.database_name,
c.service_name,
c.status,
c.last_sync_at,
COUNT(s.id) as total_scans,
COUNT(CASE WHEN s.ticket_updated = true THEN 1 END) as successful_checkins,
COUNT(CASE WHEN s.webhook_processed = true THEN 1 END) as webhook_scans,
MAX(s.scan_timestamp) as latest_scan_time
FROM codereadr_configs c
LEFT JOIN codereadr_scans s ON c.event_id = s.event_id
GROUP BY c.event_id, c.organization_id, c.database_name, c.service_name, c.status, c.last_sync_at;
-- Add RLS to the view
ALTER VIEW codereadr_scan_summary SET (security_invoker = true);
COMMENT ON TABLE codereadr_configs IS 'Configuration for CodeREADr integration per event';
COMMENT ON TABLE codereadr_scans IS 'Synchronized scan records from CodeREADr';
COMMENT ON VIEW codereadr_scan_summary IS 'Summary view of CodeREADr scan statistics per event';

View File

@@ -0,0 +1,46 @@
-- Add kiosk PIN system to events table
ALTER TABLE events ADD COLUMN IF NOT EXISTS kiosk_pin VARCHAR(4);
ALTER TABLE events ADD COLUMN IF NOT EXISTS kiosk_pin_created_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE events ADD COLUMN IF NOT EXISTS kiosk_pin_created_by UUID REFERENCES auth.users(id);
-- Add kiosk access logs table
CREATE TABLE IF NOT EXISTS kiosk_access_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
accessed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
success BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Add RLS policies for kiosk access logs
ALTER TABLE kiosk_access_logs ENABLE ROW LEVEL SECURITY;
-- Allow users to view logs for their organization's events
CREATE POLICY "Users can view kiosk logs for their organization events" ON kiosk_access_logs
FOR SELECT
TO authenticated
USING (
event_id IN (
SELECT e.id FROM events e
JOIN users u ON e.organization_id = u.organization_id
WHERE u.id = auth.uid()
)
);
-- Allow users to insert logs for their organization's events
CREATE POLICY "Users can insert kiosk logs for their organization events" ON kiosk_access_logs
FOR INSERT
TO authenticated
WITH CHECK (
event_id IN (
SELECT e.id FROM events e
JOIN users u ON e.organization_id = u.organization_id
WHERE u.id = auth.uid()
)
);
-- Add index for performance
CREATE INDEX IF NOT EXISTS idx_kiosk_access_logs_event_id ON kiosk_access_logs(event_id);
CREATE INDEX IF NOT EXISTS idx_kiosk_access_logs_accessed_at ON kiosk_access_logs(accessed_at);

View File

@@ -0,0 +1,321 @@
-- Auto-Approval Onboarding System Migration
-- This migration adds the database schema for auto-approval rules and account status tracking
-- Create auto-approval rules table
CREATE TABLE IF NOT EXISTS auto_approval_rules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
rule_name VARCHAR(100) NOT NULL,
email_domain VARCHAR(255),
business_type VARCHAR(50),
minimum_score INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Add account status and approval tracking to organizations
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS account_status VARCHAR(50) DEFAULT 'pending_approval';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS approval_score INTEGER DEFAULT 0;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS auto_approved BOOLEAN DEFAULT false;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS approved_by UUID REFERENCES users(id);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS approval_reason TEXT;
-- Add Stripe onboarding tracking
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS stripe_onboarding_status VARCHAR(50) DEFAULT 'not_started';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS stripe_onboarding_url TEXT;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS stripe_details_submitted BOOLEAN DEFAULT false;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS stripe_charges_enabled BOOLEAN DEFAULT false;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS stripe_payouts_enabled BOOLEAN DEFAULT false;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS onboarding_completed_at TIMESTAMP WITH TIME ZONE;
-- Add business profile fields for better approval scoring
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS business_type VARCHAR(50);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS business_description TEXT;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS website_url TEXT;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS address_line1 TEXT;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS address_line2 TEXT;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS city VARCHAR(100);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS state VARCHAR(50);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20);
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS country VARCHAR(2) DEFAULT 'US';
-- Create organization onboarding progress table
CREATE TABLE IF NOT EXISTS organization_onboarding_progress (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) NOT NULL,
step_key VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
completed_at TIMESTAMP WITH TIME ZONE,
data JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, step_key)
);
-- Create audit log for approval actions
CREATE TABLE IF NOT EXISTS approval_audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) NOT NULL,
action VARCHAR(50) NOT NULL, -- 'auto_approved', 'manually_approved', 'rejected'
actor_id UUID REFERENCES users(id), -- NULL for auto-approval
reason TEXT,
previous_status VARCHAR(50),
new_status VARCHAR(50),
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Insert default auto-approval rules
INSERT INTO auto_approval_rules (rule_name, email_domain, minimum_score, is_active) VALUES
('Trusted Educational Domains', 'edu', 80, true),
('Government Organizations', 'gov', 90, true),
('Known Venue Partners', 'eventbrite.com', 85, true),
('High Score Threshold', NULL, 95, true)
ON CONFLICT DO NOTHING;
-- Add indexes for performance
CREATE INDEX IF NOT EXISTS idx_organizations_account_status ON organizations(account_status);
CREATE INDEX IF NOT EXISTS idx_organizations_approval_score ON organizations(approval_score);
CREATE INDEX IF NOT EXISTS idx_organizations_stripe_onboarding_status ON organizations(stripe_onboarding_status);
CREATE INDEX IF NOT EXISTS idx_auto_approval_rules_active ON auto_approval_rules(is_active);
CREATE INDEX IF NOT EXISTS idx_onboarding_progress_org_id ON organization_onboarding_progress(organization_id);
CREATE INDEX IF NOT EXISTS idx_approval_audit_log_org_id ON approval_audit_log(organization_id);
CREATE INDEX IF NOT EXISTS idx_approval_audit_log_created_at ON approval_audit_log(created_at);
-- Create function to update updated_at timestamps
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Add triggers for updated_at
CREATE TRIGGER update_auto_approval_rules_updated_at
BEFORE UPDATE ON auto_approval_rules
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_onboarding_progress_updated_at
BEFORE UPDATE ON organization_onboarding_progress
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- RLS Policies for new tables
ALTER TABLE auto_approval_rules ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_onboarding_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE approval_audit_log ENABLE ROW LEVEL SECURITY;
-- Auto-approval rules: Only admins can access
CREATE POLICY "Admins can manage auto-approval rules" ON auto_approval_rules
FOR ALL USING (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
-- Onboarding progress: Users can access their own organization's progress
CREATE POLICY "Users can access their organization's onboarding progress" ON organization_onboarding_progress
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
-- Audit log: Users can read their own organization's audit log, admins can read all
CREATE POLICY "Users can read their organization's audit log" ON approval_audit_log
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')
);
-- Only admins can write to audit log
CREATE POLICY "Only admins can write to audit log" ON approval_audit_log
FOR INSERT WITH CHECK (auth.uid() IN (SELECT id FROM users WHERE role = 'admin'));
-- Create function to calculate approval score
CREATE OR REPLACE FUNCTION calculate_approval_score(org_id UUID)
RETURNS INTEGER AS $$
DECLARE
score INTEGER DEFAULT 0;
org_record RECORD;
user_record RECORD;
BEGIN
-- Get organization and user data
SELECT * INTO org_record FROM organizations WHERE id = org_id;
SELECT * INTO user_record FROM users WHERE organization_id = org_id AND role = 'organizer' LIMIT 1;
IF org_record IS NULL OR user_record IS NULL THEN
RETURN 0;
END IF;
-- Base score for having an organization
score := 10;
-- Email domain scoring
IF user_record.email LIKE '%.edu' THEN
score := score + 40;
ELSIF user_record.email LIKE '%.gov' THEN
score := score + 50;
ELSIF user_record.email LIKE '%.org' THEN
score := score + 20;
END IF;
-- Business information completeness
IF org_record.business_type IS NOT NULL THEN
score := score + 10;
END IF;
IF org_record.business_description IS NOT NULL AND LENGTH(org_record.business_description) > 50 THEN
score := score + 15;
END IF;
IF org_record.website_url IS NOT NULL THEN
score := score + 10;
END IF;
IF org_record.phone_number IS NOT NULL THEN
score := score + 5;
END IF;
IF org_record.address_line1 IS NOT NULL THEN
score := score + 10;
END IF;
-- Update the organization's approval score
UPDATE organizations SET approval_score = score WHERE id = org_id;
RETURN score;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create function to check if organization should be auto-approved
CREATE OR REPLACE FUNCTION should_auto_approve(org_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
score INTEGER;
org_record RECORD;
user_record RECORD;
rule_record RECORD;
BEGIN
-- Calculate current score
score := calculate_approval_score(org_id);
-- Get organization and user data
SELECT * INTO org_record FROM organizations WHERE id = org_id;
SELECT * INTO user_record FROM users WHERE organization_id = org_id AND role = 'organizer' LIMIT 1;
-- Check domain-specific rules
FOR rule_record IN SELECT * FROM auto_approval_rules WHERE is_active = true AND email_domain IS NOT NULL LOOP
IF user_record.email LIKE '%' || rule_record.email_domain THEN
IF score >= rule_record.minimum_score THEN
RETURN true;
END IF;
END IF;
END LOOP;
-- Check general score threshold rules
FOR rule_record IN SELECT * FROM auto_approval_rules WHERE is_active = true AND email_domain IS NULL LOOP
IF score >= rule_record.minimum_score THEN
RETURN true;
END IF;
END LOOP;
RETURN false;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create function to process auto-approval
CREATE OR REPLACE FUNCTION process_auto_approval(org_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
should_approve BOOLEAN;
current_status VARCHAR(50);
BEGIN
-- Get current status
SELECT account_status INTO current_status FROM organizations WHERE id = org_id;
-- Only process if currently pending approval
IF current_status != 'pending_approval' THEN
RETURN false;
END IF;
-- Check if should auto-approve
should_approve := should_auto_approve(org_id);
IF should_approve THEN
-- Update organization status
UPDATE organizations SET
account_status = 'approved',
auto_approved = true,
approved_at = NOW(),
approval_reason = 'Auto-approved based on scoring rules'
WHERE id = org_id;
-- Log the approval
INSERT INTO approval_audit_log (
organization_id,
action,
actor_id,
reason,
previous_status,
new_status,
metadata
) VALUES (
org_id,
'auto_approved',
NULL,
'Auto-approved based on scoring rules',
'pending_approval',
'approved',
jsonb_build_object('approval_score', (SELECT approval_score FROM organizations WHERE id = org_id))
);
RETURN true;
END IF;
RETURN false;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create trigger to process auto-approval when organization is created or updated
CREATE OR REPLACE FUNCTION trigger_auto_approval_check()
RETURNS TRIGGER AS $$
BEGIN
-- Only process if account_status is pending_approval
IF NEW.account_status = 'pending_approval' THEN
PERFORM process_auto_approval(NEW.id);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Add trigger to organizations table
CREATE TRIGGER check_auto_approval_on_update
AFTER UPDATE ON organizations
FOR EACH ROW
EXECUTE FUNCTION trigger_auto_approval_check();
-- Create view for admin dashboard
CREATE OR REPLACE VIEW admin_pending_approvals AS
SELECT
o.id,
o.name,
o.account_status,
o.approval_score,
o.business_type,
o.business_description,
o.created_at,
u.email as owner_email,
u.name as owner_name,
calculate_approval_score(o.id) as current_score,
should_auto_approve(o.id) as can_auto_approve
FROM organizations o
JOIN users u ON u.organization_id = o.id AND u.role = 'organizer'
WHERE o.account_status = 'pending_approval'
ORDER BY o.created_at DESC;
-- Grant permissions to authenticated users
GRANT SELECT ON admin_pending_approvals TO authenticated;
GRANT EXECUTE ON FUNCTION calculate_approval_score TO authenticated;
GRANT EXECUTE ON FUNCTION should_auto_approve TO authenticated;
GRANT EXECUTE ON FUNCTION process_auto_approval TO authenticated;

View File

@@ -0,0 +1,297 @@
-- Territory Manager System Schema
-- This migration creates the core tables for the Territory Manager system
-- Create territories table
CREATE TABLE IF NOT EXISTS territories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
boundary JSONB, -- GeoJSON polygon for territory bounds
population INTEGER,
market_size VARCHAR(50),
assigned_to UUID REFERENCES users(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create territory_managers table
CREATE TABLE IF NOT EXISTS territory_managers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) UNIQUE NOT NULL,
territory_id UUID REFERENCES territories(id),
referral_code VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended', 'inactive')),
application_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
approved_date TIMESTAMP WITH TIME ZONE,
approved_by UUID REFERENCES users(id),
profile JSONB, -- Personal info, preferences, etc.
documents JSONB, -- Uploaded documents like ID, W-9, etc.
earnings_data JSONB, -- Earnings statistics and history
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create territory_applications table
CREATE TABLE IF NOT EXISTS territory_applications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
phone VARCHAR(20),
address JSONB, -- Address information
desired_territory VARCHAR(255),
has_transportation BOOLEAN DEFAULT false,
has_event_experience BOOLEAN DEFAULT false,
motivation TEXT,
documents JSONB, -- File uploads
consent JSONB, -- Background check and data processing consent
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMP WITH TIME ZONE,
review_notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create leads table
CREATE TABLE IF NOT EXISTS leads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
event_name VARCHAR(255) NOT NULL,
organizer_contact JSONB, -- Contact information
event_details JSONB, -- Event details like date, type, etc.
status VARCHAR(20) DEFAULT 'cold' CHECK (status IN ('cold', 'contacted', 'confirmed', 'converted', 'dead')),
notes TEXT[],
follow_up_date TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create commissions table
CREATE TABLE IF NOT EXISTS commissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
event_id UUID REFERENCES events(id) NOT NULL,
tickets_sold INTEGER DEFAULT 0,
commission_per_ticket DECIMAL(10,2) DEFAULT 0.10,
total_commission DECIMAL(10,2) DEFAULT 0,
status VARCHAR(20) DEFAULT 'unpaid' CHECK (status IN ('unpaid', 'paid', 'pending')),
payout_date TIMESTAMP WITH TIME ZONE,
stripe_transfer_id VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create training_progress table
CREATE TABLE IF NOT EXISTS training_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
module_id VARCHAR(255) NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
score INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(territory_manager_id, module_id)
);
-- Create achievements table
CREATE TABLE IF NOT EXISTS achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
requirements JSONB, -- Achievement requirements
reward_amount DECIMAL(10,2) DEFAULT 0,
badge_icon VARCHAR(255),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create territory_manager_achievements table
CREATE TABLE IF NOT EXISTS territory_manager_achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
achievement_id UUID REFERENCES achievements(id) NOT NULL,
earned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(territory_manager_id, achievement_id)
);
-- Create expense_reports table
CREATE TABLE IF NOT EXISTS expense_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
event_id UUID REFERENCES events(id),
mileage DECIMAL(10,2) DEFAULT 0,
receipts JSONB, -- File uploads
total_amount DECIMAL(10,2) DEFAULT 0,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'paid', 'rejected')),
submitted_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
approved_date TIMESTAMP WITH TIME ZONE,
approved_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create notifications table
CREATE TABLE IF NOT EXISTS tm_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
territory_manager_id UUID REFERENCES territory_managers(id) NOT NULL,
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT,
data JSONB, -- Additional notification data
read BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes for better performance
CREATE INDEX idx_territories_assigned_to ON territories(assigned_to);
CREATE INDEX idx_territory_managers_user_id ON territory_managers(user_id);
CREATE INDEX idx_territory_managers_territory_id ON territory_managers(territory_id);
CREATE INDEX idx_territory_managers_status ON territory_managers(status);
CREATE INDEX idx_territory_applications_status ON territory_applications(status);
CREATE INDEX idx_territory_applications_email ON territory_applications(email);
CREATE INDEX idx_leads_territory_manager_id ON leads(territory_manager_id);
CREATE INDEX idx_leads_status ON leads(status);
CREATE INDEX idx_commissions_territory_manager_id ON commissions(territory_manager_id);
CREATE INDEX idx_commissions_event_id ON commissions(event_id);
CREATE INDEX idx_commissions_status ON commissions(status);
CREATE INDEX idx_training_progress_territory_manager_id ON training_progress(territory_manager_id);
CREATE INDEX idx_tm_notifications_territory_manager_id ON tm_notifications(territory_manager_id);
CREATE INDEX idx_tm_notifications_read ON tm_notifications(read);
-- Add RLS policies
ALTER TABLE territories ENABLE ROW LEVEL SECURITY;
ALTER TABLE territory_managers ENABLE ROW LEVEL SECURITY;
ALTER TABLE territory_applications ENABLE ROW LEVEL SECURITY;
ALTER TABLE leads ENABLE ROW LEVEL SECURITY;
ALTER TABLE commissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE training_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE achievements ENABLE ROW LEVEL SECURITY;
ALTER TABLE territory_manager_achievements ENABLE ROW LEVEL SECURITY;
ALTER TABLE expense_reports ENABLE ROW LEVEL SECURITY;
ALTER TABLE tm_notifications ENABLE ROW LEVEL SECURITY;
-- Basic RLS policies (admin can see all, territory managers can only see their own data)
CREATE POLICY "Admin can view all territories" ON territories FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can view their territory" ON territories FOR SELECT USING (assigned_to = auth.uid());
CREATE POLICY "Admin can view all territory managers" ON territory_managers FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can view their own data" ON territory_managers FOR SELECT USING (user_id = auth.uid());
CREATE POLICY "Territory managers can update their own data" ON territory_managers FOR UPDATE USING (user_id = auth.uid());
CREATE POLICY "Admin can view all applications" ON territory_applications FOR ALL USING (is_admin());
CREATE POLICY "Public can create applications" ON territory_applications FOR INSERT WITH CHECK (true);
CREATE POLICY "Admin can view all leads" ON leads FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can manage their leads" ON leads FOR ALL USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = leads.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Admin can view all commissions" ON commissions FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can view their commissions" ON commissions FOR SELECT USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = commissions.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Admin can view all training progress" ON training_progress FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can manage their training progress" ON training_progress FOR ALL USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = training_progress.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Everyone can view achievements" ON achievements FOR SELECT USING (true);
CREATE POLICY "Admin can manage achievements" ON achievements FOR ALL USING (is_admin());
CREATE POLICY "Admin can view all territory manager achievements" ON territory_manager_achievements FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can view their achievements" ON territory_manager_achievements FOR SELECT USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = territory_manager_achievements.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Admin can view all expense reports" ON expense_reports FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can manage their expense reports" ON expense_reports FOR ALL USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = expense_reports.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Admin can view all notifications" ON tm_notifications FOR ALL USING (is_admin());
CREATE POLICY "Territory managers can view their notifications" ON tm_notifications FOR SELECT USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = tm_notifications.territory_manager_id AND territory_managers.user_id = auth.uid())
);
CREATE POLICY "Territory managers can update their notifications" ON tm_notifications FOR UPDATE USING (
EXISTS (SELECT 1 FROM territory_managers WHERE territory_managers.id = tm_notifications.territory_manager_id AND territory_managers.user_id = auth.uid())
);
-- Create trigger for updating updated_at timestamps
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_territories_updated_at BEFORE UPDATE ON territories
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_territory_managers_updated_at BEFORE UPDATE ON territory_managers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_territory_applications_updated_at BEFORE UPDATE ON territory_applications
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_leads_updated_at BEFORE UPDATE ON leads
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_commissions_updated_at BEFORE UPDATE ON commissions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_expense_reports_updated_at BEFORE UPDATE ON expense_reports
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to generate unique referral codes
CREATE OR REPLACE FUNCTION generate_referral_code() RETURNS TEXT AS $$
DECLARE
code TEXT;
exists_check INTEGER;
BEGIN
LOOP
code := UPPER(SUBSTRING(MD5(random()::text) FROM 1 FOR 8));
SELECT COUNT(*) INTO exists_check FROM territory_managers WHERE referral_code = code;
IF exists_check = 0 THEN
RETURN code;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Function to auto-assign referral code when creating territory manager
CREATE OR REPLACE FUNCTION auto_assign_referral_code() RETURNS TRIGGER AS $$
BEGIN
IF NEW.referral_code IS NULL OR NEW.referral_code = '' THEN
NEW.referral_code := generate_referral_code();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER auto_assign_referral_code_trigger
BEFORE INSERT ON territory_managers
FOR EACH ROW
EXECUTE FUNCTION auto_assign_referral_code();
-- Function to calculate commission totals
CREATE OR REPLACE FUNCTION calculate_commission_total() RETURNS TRIGGER AS $$
BEGIN
NEW.total_commission := NEW.tickets_sold * NEW.commission_per_ticket;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER calculate_commission_total_trigger
BEFORE INSERT OR UPDATE ON commissions
FOR EACH ROW
EXECUTE FUNCTION calculate_commission_total();
-- Insert sample achievements
INSERT INTO achievements (name, description, requirements, reward_amount, badge_icon) VALUES
('First Referral', 'Successfully refer your first event to BCT', '{"referrals": 1}', 50.00, 'first-referral.svg'),
('Sales Champion', 'Refer 10 events in a single month', '{"monthly_referrals": 10}', 500.00, 'sales-champion.svg'),
('Territory Master', 'Maintain 95% event success rate for 3 months', '{"success_rate": 0.95, "months": 3}', 1000.00, 'territory-master.svg'),
('Training Graduate', 'Complete all training modules', '{"training_modules": "all"}', 100.00, 'training-graduate.svg'),
('Community Builder', 'Refer 5 recurring events', '{"recurring_events": 5}', 300.00, 'community-builder.svg'),
('Revenue Generator', 'Generate over $10,000 in commission in a quarter', '{"quarterly_commission": 10000}', 2000.00, 'revenue-generator.svg');

View File

@@ -0,0 +1,192 @@
-- Custom Page Templates Schema
-- Reusable page templates that can be applied to multiple events
CREATE TABLE custom_page_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- Template configuration
name VARCHAR(255) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT true,
-- Craft.js configuration
page_data JSONB NOT NULL DEFAULT '{}', -- Stores the Craft.js page structure
custom_css TEXT, -- Additional custom CSS
-- Template preview
preview_image_url TEXT,
-- Tracking
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- Custom Sales Pages Schema
-- Links events to custom page templates with custom URLs
CREATE TABLE custom_sales_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
template_id UUID REFERENCES custom_page_templates(id) ON DELETE SET NULL,
-- Custom URL configuration
custom_slug VARCHAR(100) NOT NULL UNIQUE, -- For blackcanyontickets.com/xxxx
is_active BOOLEAN DEFAULT false,
is_default BOOLEAN DEFAULT false, -- If true, this page is used instead of default /e/[slug]
-- Page-specific overrides (if different from template)
page_data JSONB, -- If set, overrides template page_data
custom_css TEXT, -- Additional CSS on top of template
-- SEO and metadata
meta_title VARCHAR(255),
meta_description TEXT,
og_image_url TEXT,
-- Analytics
view_count INTEGER DEFAULT 0,
last_viewed_at TIMESTAMP WITH TIME ZONE,
-- Tracking
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- Custom Pricing System Schema
-- Special pricing controls for superusers
CREATE TABLE custom_pricing_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
-- Stripe account configuration
stripe_account_id VARCHAR(255), -- Personal Stripe account ID
use_personal_stripe BOOLEAN DEFAULT false,
-- Pricing overrides
can_override_pricing BOOLEAN DEFAULT false,
can_set_custom_fees BOOLEAN DEFAULT false,
-- Custom fee structure
custom_platform_fee_type VARCHAR(20) CHECK (custom_platform_fee_type IN ('percentage', 'fixed', 'none')),
custom_platform_fee_percentage DECIMAL(5,2),
custom_platform_fee_fixed INTEGER, -- in cents
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Event-specific pricing overrides
CREATE TABLE event_pricing_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
custom_pricing_profile_id UUID NOT NULL REFERENCES custom_pricing_profiles(id) ON DELETE CASCADE,
-- Override settings
use_custom_stripe_account BOOLEAN DEFAULT false,
override_platform_fees BOOLEAN DEFAULT false,
-- Custom fee values for this event
platform_fee_type VARCHAR(20) CHECK (platform_fee_type IN ('percentage', 'fixed', 'none')),
platform_fee_percentage DECIMAL(5,2),
platform_fee_fixed INTEGER, -- in cents
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- RLS Policies
ALTER TABLE custom_page_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE custom_sales_pages ENABLE ROW LEVEL SECURITY;
ALTER TABLE custom_pricing_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE event_pricing_overrides ENABLE ROW LEVEL SECURITY;
-- Custom page templates policies
CREATE POLICY "Users can view custom page templates for their organization" ON custom_page_templates
FOR SELECT USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
CREATE POLICY "Users can manage custom page templates for their organization" ON custom_page_templates
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
-- Custom sales pages policies
CREATE POLICY "Users can view custom sales pages for their organization" ON custom_sales_pages
FOR SELECT USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
CREATE POLICY "Users can manage custom sales pages for their organization" ON custom_sales_pages
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM users WHERE id = auth.uid()
)
);
-- Custom pricing profiles policies - restricted to superusers
CREATE POLICY "Only superusers can view custom pricing profiles" ON custom_pricing_profiles
FOR SELECT USING (
user_id = auth.uid() AND
auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
);
CREATE POLICY "Only superusers can manage custom pricing profiles" ON custom_pricing_profiles
FOR ALL USING (
user_id = auth.uid() AND
auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
);
-- Event pricing overrides policies
CREATE POLICY "Users can view event pricing overrides for their events" ON event_pricing_overrides
FOR SELECT USING (
event_id IN (
SELECT e.id FROM events e
JOIN users u ON e.organization_id = u.organization_id
WHERE u.id = auth.uid()
)
);
CREATE POLICY "Only superusers can manage event pricing overrides" ON event_pricing_overrides
FOR ALL USING (
custom_pricing_profile_id IN (
SELECT id FROM custom_pricing_profiles
WHERE user_id = auth.uid() AND
auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
)
);
-- Indexes for performance
CREATE INDEX idx_custom_page_templates_organization_id ON custom_page_templates(organization_id);
CREATE INDEX idx_custom_page_templates_active ON custom_page_templates(is_active) WHERE is_active = true;
CREATE INDEX idx_custom_sales_pages_organization_id ON custom_sales_pages(organization_id);
CREATE INDEX idx_custom_sales_pages_event_id ON custom_sales_pages(event_id);
CREATE INDEX idx_custom_sales_pages_template_id ON custom_sales_pages(template_id);
CREATE INDEX idx_custom_sales_pages_slug ON custom_sales_pages(custom_slug) WHERE is_active = true;
CREATE INDEX idx_custom_sales_pages_active ON custom_sales_pages(is_active) WHERE is_active = true;
CREATE INDEX idx_custom_pricing_profiles_user_id ON custom_pricing_profiles(user_id);
CREATE INDEX idx_event_pricing_overrides_event_id ON event_pricing_overrides(event_id);
-- Insert initial custom pricing profiles for superusers
INSERT INTO custom_pricing_profiles (user_id, can_override_pricing, can_set_custom_fees)
SELECT
id,
true,
true
FROM users
WHERE role = 'admin'
ON CONFLICT (user_id) DO UPDATE SET
can_override_pricing = true,
can_set_custom_fees = true;