-- Migration: 001_initial_schema.sql -- Created: 2024 -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================ -- PLATFORM ADMINS (internal team) -- ============================================ CREATE TABLE platform_admins ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'support' CHECK (role IN ('super_admin', 'support', 'sales')), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_login_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_platform_admins_email ON platform_admins(email); -- ============================================ -- MODULES (global module definitions) -- ============================================ CREATE TABLE modules ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), key VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, monthly_price INTEGER NOT NULL DEFAULT 0, -- cent is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Insert default modules INSERT INTO modules (key, name, description, monthly_price) VALUES ('office', 'Office', 'Kundenverwaltung, Fahrzeugverwaltung, Berichte', 0), ('calendar', 'Kalender', 'Terminplanung (Integration fahrschultermin.de)', 0), ('messenger', 'Messenger', 'WhatsApp-ähnlicher Messenger', 0), ('learning_blank', 'Lernsoftware (Blanko)', 'Eigene Inhalte erstellen', 0), ('learning_premium', 'Lernsoftware (Premium)', 'Inkl. vorgefertigter Lerninhalte', 0); -- ============================================ -- PACKAGES (pricing bundles) -- ============================================ CREATE TABLE packages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), key VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, monthly_price INTEGER NOT NULL DEFAULT 0, -- cent modules_included JSONB DEFAULT '[]', max_users INTEGER DEFAULT 5, max_branches INTEGER DEFAULT 1, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Insert default packages INSERT INTO packages (key, name, description, monthly_price, modules_included, max_users, max_branches) VALUES ('starter', 'Starter', 'Basic-Paket für kleine Fahrschulen', 4900, '["office", "calendar", "messenger"]', 5, 1), ('professional', 'Professional', 'Für mittlere Fahrschulen', 9900, '["office", "calendar", "messenger", "learning_blank"]', 20, 3), ('premium', 'Premium', 'Alle Funktionen inkl. Premium-Lerninhalte', 14900, '["office", "calendar", "messenger", "learning_premium"]', 100, 10); -- ============================================ -- TENANTS (fahrschulen / customers) -- ============================================ CREATE TABLE tenants ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL, phone VARCHAR(100), address TEXT, status VARCHAR(50) NOT NULL DEFAULT 'pending_approval' CHECK (status IN ('pending_approval', 'active', 'suspended', 'trial')), trial_ends_at TIMESTAMP WITH TIME ZONE, is_self_hosted BOOLEAN DEFAULT FALSE, self_hosted_license_key VARCHAR(255), -- Branding logo_url VARCHAR(500), primary_color VARCHAR(7) DEFAULT '#2563eb', secondary_color VARCHAR(7) DEFAULT '#64748b', custom_css TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_tenants_slug ON tenants(slug); CREATE INDEX idx_tenants_status ON tenants(status); CREATE INDEX idx_tenants_email ON tenants(email); -- ============================================ -- BRANCHES (filialen) -- ============================================ CREATE TABLE branches ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, address TEXT, phone VARCHAR(100), email VARCHAR(255), opening_hours JSONB DEFAULT '{}', is_headquarters BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_branches_tenant ON branches(tenant_id); -- ============================================ -- USERS (tenant users) -- ============================================ CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, branch_id UUID REFERENCES branches(id) ON DELETE SET NULL, email VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, user_type VARCHAR(50) NOT NULL DEFAULT 'student' CHECK (user_type IN ('admin', 'office', 'instructor', 'lecturer', 'student')), status VARCHAR(50) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'pending')), avatar_url VARCHAR(500), phone VARCHAR(100), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_login_at TIMESTAMP WITH TIME ZONE, UNIQUE(tenant_id, email) ); CREATE INDEX idx_users_tenant ON users(tenant_id); CREATE INDEX idx_users_branch ON users(branch_id); CREATE INDEX idx_users_email ON users(email); -- ============================================ -- TENANT MODULES (module activation per tenant) -- ============================================ CREATE TABLE tenant_modules ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, module_id UUID NOT NULL REFERENCES modules(id) ON DELETE RESTRICT, status VARCHAR(50) NOT NULL DEFAULT 'enabled' CHECK (status IN ('enabled', 'disabled')), enabled_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP WITH TIME ZONE, trial_ends_at TIMESTAMP WITH TIME ZONE, UNIQUE(tenant_id, module_id) ); CREATE INDEX idx_tenant_modules_tenant ON tenant_modules(tenant_id); -- ============================================ -- TENANT PACKAGES (active package per tenant) -- ============================================ CREATE TABLE tenant_packages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, package_id UUID NOT NULL REFERENCES packages(id) ON DELETE RESTRICT, started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP WITH TIME ZONE, UNIQUE(tenant_id) ); CREATE INDEX idx_tenant_packages_tenant ON tenant_packages(tenant_id); -- ============================================ -- LEARNING CONTENTS -- ============================================ CREATE TABLE learning_contents ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = platform global category VARCHAR(255) NOT NULL, title VARCHAR(500) NOT NULL, description TEXT, content_type VARCHAR(50) NOT NULL CHECK (content_type IN ('video', 'document', 'quiz', 'exercise')), content_url VARCHAR(500), thumbnail_url VARCHAR(500), duration_minutes INTEGER, is_premium BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_learning_contents_tenant ON learning_contents(tenant_id); CREATE INDEX idx_learning_contents_category ON learning_contents(category); -- ============================================ -- STUDENT PROGRESS -- ============================================ CREATE TABLE student_progress ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, content_id UUID NOT NULL REFERENCES learning_contents(id) ON DELETE CASCADE, status VARCHAR(50) NOT NULL DEFAULT 'not_started' CHECK (status IN ('not_started', 'in_progress', 'completed')), completed_at TIMESTAMP WITH TIME ZONE, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, content_id) ); CREATE INDEX idx_student_progress_user ON student_progress(user_id); CREATE INDEX idx_student_progress_content ON student_progress(content_id); -- ============================================ -- AUDIT LOGS -- ============================================ CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = platform admin action user_id UUID, -- can be NULL if system action action VARCHAR(100) NOT NULL, entity_type VARCHAR(100) NOT NULL, entity_id UUID, old_values JSONB, new_values JSONB, ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_audit_logs_tenant ON audit_logs(tenant_id); CREATE INDEX idx_audit_logs_created ON audit_logs(created_at); CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id); -- ============================================ -- INVOICES -- ============================================ CREATE TABLE invoices ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, invoice_number VARCHAR(100) NOT NULL UNIQUE, amount INTEGER NOT NULL, -- cent vat_amount INTEGER NOT NULL DEFAULT 0, total_amount INTEGER NOT NULL, currency VARCHAR(3) DEFAULT 'EUR', status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('draft', 'pending', 'paid', 'overdue', 'cancelled')), billing_period_start DATE, billing_period_end DATE, due_date DATE, paid_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_invoices_tenant ON invoices(tenant_id); CREATE INDEX idx_invoices_status ON invoices(status); CREATE INDEX idx_invoices_due_date ON invoices(due_date); CREATE TABLE invoice_items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, description TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, unit_price INTEGER NOT NULL, -- cent total_price INTEGER NOT NULL ); CREATE INDEX idx_invoice_items_invoice ON invoice_items(invoice_id); -- ============================================ -- NOTIFICATIONS -- ============================================ CREATE TABLE notification_templates ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), key VARCHAR(100) NOT NULL UNIQUE, subject VARCHAR(500) NOT NULL, body_html TEXT, body_text TEXT, variables JSONB DEFAULT '[]', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE notifications ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, template_key VARCHAR(100), subject VARCHAR(500) NOT NULL, body TEXT, status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')), sent_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_notifications_user ON notifications(user_id); CREATE INDEX idx_notifications_tenant ON notifications(tenant_id); -- ============================================ -- FILES (DKS) -- ============================================ CREATE TABLE files ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE SET NULL, entity_type VARCHAR(100) NOT NULL, entity_id UUID, file_type VARCHAR(100) NOT NULL, original_filename VARCHAR(500) NOT NULL, storage_path VARCHAR(1000) NOT NULL, mime_type VARCHAR(255), size_bytes BIGINT, expires_at TIMESTAMP WITH TIME ZONE, -- for temp files created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_files_tenant ON files(tenant_id); CREATE INDEX idx_files_entity ON files(entity_type, entity_id); CREATE INDEX idx_files_user ON files(user_id); -- ============================================ -- MESSENGER -- ============================================ CREATE TABLE conversations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL DEFAULT 'direct' CHECK (type IN ('direct', 'group')), name VARCHAR(255), -- only for groups created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_conversations_tenant ON conversations(tenant_id); CREATE TABLE conversation_participants ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_read_at TIMESTAMP WITH TIME ZONE, UNIQUE(conversation_id, user_id) ); CREATE INDEX idx_conversation_participants_conversation ON conversation_participants(conversation_id); CREATE INDEX idx_conversation_participants_user ON conversation_participants(user_id); CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, body TEXT NOT NULL, is_edited BOOLEAN DEFAULT FALSE, is_deleted BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_messages_conversation ON messages(conversation_id); CREATE INDEX idx_messages_created ON messages(created_at); -- ============================================ -- UPDATE TIMESTAMP TRIGGER -- ============================================ CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER tr_platform_admins_updated_at BEFORE UPDATE ON platform_admins FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_tenants_updated_at BEFORE UPDATE ON tenants FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_branches_updated_at BEFORE UPDATE ON branches FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_learning_contents_updated_at BEFORE UPDATE ON learning_contents FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_student_progress_updated_at BEFORE UPDATE ON student_progress FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_invoices_updated_at BEFORE UPDATE ON invoices FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_conversations_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_messages_updated_at BEFORE UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_updated_at();