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:
125
supabase/migrations/011_add_super_admin_support.sql
Normal file
125
supabase/migrations/011_add_super_admin_support.sql
Normal 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)';
|
||||
579
supabase/migrations/20250109_add_sample_territories.sql
Normal file
579
supabase/migrations/20250109_add_sample_territories.sql
Normal 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';
|
||||
186
supabase/migrations/20250109_codereadr_integration.sql
Normal file
186
supabase/migrations/20250109_codereadr_integration.sql
Normal 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';
|
||||
46
supabase/migrations/20250109_kiosk_pin_system.sql
Normal file
46
supabase/migrations/20250109_kiosk_pin_system.sql
Normal 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);
|
||||
321
supabase/migrations/20250109_onboarding_system.sql
Normal file
321
supabase/migrations/20250109_onboarding_system.sql
Normal 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;
|
||||
297
supabase/migrations/20250109_territory_manager_schema.sql
Normal file
297
supabase/migrations/20250109_territory_manager_schema.sql
Normal 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');
|
||||
192
supabase/migrations/20250110_custom_sales_pages.sql
Normal file
192
supabase/migrations/20250110_custom_sales_pages.sql
Normal 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;
|
||||
Reference in New Issue
Block a user