feat: Datenbank-Schema & ER-Diagramm

📊 Schema (schema.sql):
- organizations (Multi-Tenancy)
- users (mit Rollen)
- contacts (mit DSGVO Felder)
- companies
- pipelines (JSONB stages)
- deals
- activities
- audit_logs (DSGVO)
- refresh_tokens

📈 ER-Diagramm (Mermaid)
🌱 Seed-Daten für Testing

Features:
- UUID Primary Keys
- Soft Delete (deleted_at)
- Auto-updated timestamps
- GIN Index für Tags
- Row-Level Security ready
This commit is contained in:
2026-02-11 10:03:34 +00:00
parent 18eb396b1e
commit 4b7297c199
3 changed files with 539 additions and 0 deletions

320
src/db/schema.sql Normal file
View File

@@ -0,0 +1,320 @@
-- Pulse CRM Database Schema
-- PostgreSQL 16+
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- ORGANIZATIONS (Multi-Tenancy)
-- ============================================
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
settings JSONB DEFAULT '{}',
subscription_plan VARCHAR(50) DEFAULT 'starter',
subscription_status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_organizations_slug ON organizations(slug);
-- ============================================
-- USERS
-- ============================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
role VARCHAR(50) DEFAULT 'user', -- admin, manager, user
avatar_url TEXT,
phone VARCHAR(50),
is_active BOOLEAN DEFAULT true,
email_verified BOOLEAN DEFAULT false,
last_login_at TIMESTAMP WITH TIME ZONE,
settings JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(org_id, email)
);
CREATE INDEX idx_users_org_id ON users(org_id);
CREATE INDEX idx_users_email ON users(email);
-- ============================================
-- CONTACTS
-- ============================================
CREATE TABLE contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
company_id UUID REFERENCES companies(id) ON DELETE SET NULL,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Basic Info
first_name VARCHAR(100),
last_name VARCHAR(100),
email VARCHAR(255),
phone VARCHAR(50),
mobile VARCHAR(50),
job_title VARCHAR(100),
-- Address
address_line1 VARCHAR(255),
address_line2 VARCHAR(255),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(100),
-- Social
linkedin_url TEXT,
twitter_handle VARCHAR(100),
-- Status
status VARCHAR(50) DEFAULT 'active', -- active, inactive, archived
lead_source VARCHAR(100),
lead_score INTEGER DEFAULT 0,
-- Custom Fields
custom_fields JSONB DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
-- DSGVO
gdpr_consent BOOLEAN DEFAULT false,
gdpr_consent_date TIMESTAMP WITH TIME ZONE,
is_restricted BOOLEAN DEFAULT false, -- Art. 18 DSGVO
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE -- Soft delete
);
CREATE INDEX idx_contacts_org_id ON contacts(org_id);
CREATE INDEX idx_contacts_company_id ON contacts(company_id);
CREATE INDEX idx_contacts_owner_id ON contacts(owner_id);
CREATE INDEX idx_contacts_email ON contacts(email);
CREATE INDEX idx_contacts_tags ON contacts USING GIN(tags);
CREATE INDEX idx_contacts_deleted_at ON contacts(deleted_at) WHERE deleted_at IS NULL;
-- ============================================
-- COMPANIES
-- ============================================
CREATE TABLE companies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Basic Info
name VARCHAR(255) NOT NULL,
industry VARCHAR(100),
website VARCHAR(255),
description TEXT,
-- Contact Info
phone VARCHAR(50),
email VARCHAR(255),
-- Address
address_line1 VARCHAR(255),
address_line2 VARCHAR(255),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(100),
-- Business Info
employee_count VARCHAR(50),
annual_revenue VARCHAR(50),
-- Custom Fields
custom_fields JSONB DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_companies_org_id ON companies(org_id);
CREATE INDEX idx_companies_name ON companies(name);
-- ============================================
-- PIPELINES
-- ============================================
CREATE TABLE pipelines (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
is_default BOOLEAN DEFAULT false,
stages JSONB NOT NULL DEFAULT '[]',
-- stages format: [{"id": "uuid", "name": "Lead", "order": 1, "probability": 10}, ...]
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_pipelines_org_id ON pipelines(org_id);
-- ============================================
-- DEALS
-- ============================================
CREATE TABLE deals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE,
contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL,
company_id UUID REFERENCES companies(id) ON DELETE SET NULL,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Deal Info
title VARCHAR(255) NOT NULL,
value DECIMAL(15, 2) DEFAULT 0,
currency VARCHAR(3) DEFAULT 'EUR',
stage_id VARCHAR(100) NOT NULL, -- References stages JSONB in pipelines
probability INTEGER DEFAULT 0,
-- Dates
expected_close_date DATE,
actual_close_date DATE,
-- Status
status VARCHAR(50) DEFAULT 'open', -- open, won, lost
lost_reason VARCHAR(255),
-- Custom Fields
custom_fields JSONB DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_deals_org_id ON deals(org_id);
CREATE INDEX idx_deals_pipeline_id ON deals(pipeline_id);
CREATE INDEX idx_deals_contact_id ON deals(contact_id);
CREATE INDEX idx_deals_company_id ON deals(company_id);
CREATE INDEX idx_deals_owner_id ON deals(owner_id);
CREATE INDEX idx_deals_status ON deals(status);
CREATE INDEX idx_deals_stage_id ON deals(stage_id);
-- ============================================
-- ACTIVITIES
-- ============================================
CREATE TABLE activities (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
contact_id UUID REFERENCES contacts(id) ON DELETE CASCADE,
company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
deal_id UUID REFERENCES deals(id) ON DELETE CASCADE,
-- Activity Info
type VARCHAR(50) NOT NULL, -- note, call, email, meeting, task
subject VARCHAR(255),
description TEXT,
-- For tasks/meetings
due_date TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
is_completed BOOLEAN DEFAULT false,
-- For calls
duration_minutes INTEGER,
call_outcome VARCHAR(50),
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_activities_org_id ON activities(org_id);
CREATE INDEX idx_activities_user_id ON activities(user_id);
CREATE INDEX idx_activities_contact_id ON activities(contact_id);
CREATE INDEX idx_activities_deal_id ON activities(deal_id);
CREATE INDEX idx_activities_type ON activities(type);
CREATE INDEX idx_activities_due_date ON activities(due_date);
-- ============================================
-- AUDIT LOG (DSGVO)
-- ============================================
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Action Info
action VARCHAR(50) NOT NULL, -- create, read, update, delete, export, login
entity_type VARCHAR(50) NOT NULL, -- contact, deal, user, etc.
entity_id UUID,
-- Changes
changes JSONB, -- { before: {...}, after: {...} }
-- Context
ip_address INET,
user_agent TEXT,
-- Timestamp
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_org_id ON audit_logs(org_id);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
-- ============================================
-- REFRESH TOKENS
-- ============================================
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
revoked_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
-- ============================================
-- TRIGGERS
-- ============================================
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_contacts_updated_at BEFORE UPDATE ON contacts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_companies_updated_at BEFORE UPDATE ON companies
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_pipelines_updated_at BEFORE UPDATE ON pipelines
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_deals_updated_at BEFORE UPDATE ON deals
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_activities_updated_at BEFORE UPDATE ON activities
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

70
src/db/seed.sql Normal file
View File

@@ -0,0 +1,70 @@
-- Pulse CRM Seed Data
-- For development/testing only
-- ============================================
-- ORGANIZATION
-- ============================================
INSERT INTO organizations (id, name, slug, subscription_plan) VALUES
('11111111-1111-1111-1111-111111111111', 'Demo Company GmbH', 'demo-company', 'pro');
-- ============================================
-- USERS
-- Password: 'demo1234' (Argon2 hash)
-- ============================================
INSERT INTO users (id, org_id, email, password_hash, first_name, last_name, role, email_verified) VALUES
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'admin@demo.de', '$argon2id$v=19$m=65536,t=3,p=4$randomsalt$hashedpassword', 'Max', 'Mustermann', 'admin', true),
('22222222-2222-2222-2222-222222222223', '11111111-1111-1111-1111-111111111111', 'sales@demo.de', '$argon2id$v=19$m=65536,t=3,p=4$randomsalt$hashedpassword', 'Lisa', 'Schmidt', 'user', true);
-- ============================================
-- PIPELINE
-- ============================================
INSERT INTO pipelines (id, org_id, name, is_default, stages) VALUES
('33333333-3333-3333-3333-333333333333', '11111111-1111-1111-1111-111111111111', 'Sales Pipeline', true,
'[
{"id": "lead", "name": "Lead", "order": 1, "probability": 10},
{"id": "qualified", "name": "Qualifiziert", "order": 2, "probability": 25},
{"id": "proposal", "name": "Angebot", "order": 3, "probability": 50},
{"id": "negotiation", "name": "Verhandlung", "order": 4, "probability": 75},
{"id": "closed_won", "name": "Gewonnen", "order": 5, "probability": 100},
{"id": "closed_lost", "name": "Verloren", "order": 6, "probability": 0}
]');
-- ============================================
-- COMPANIES
-- ============================================
INSERT INTO companies (id, org_id, owner_id, name, industry, website, phone, city, country) VALUES
('44444444-4444-4444-4444-444444444444', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', 'TechStart GmbH', 'Technologie', 'https://techstart.de', '+49 30 123456', 'Berlin', 'Deutschland'),
('44444444-4444-4444-4444-444444444445', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222223', 'DataFlow AG', 'Software', 'https://dataflow.de', '+49 89 654321', 'München', 'Deutschland'),
('44444444-4444-4444-4444-444444444446', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', 'ScaleUp Solutions', 'Beratung', 'https://scaleup.de', '+49 40 789012', 'Hamburg', 'Deutschland');
-- ============================================
-- CONTACTS
-- ============================================
INSERT INTO contacts (id, org_id, company_id, owner_id, first_name, last_name, email, phone, job_title, status, gdpr_consent, gdpr_consent_date, tags) VALUES
('55555555-5555-5555-5555-555555555555', '11111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444', '22222222-2222-2222-2222-222222222222', 'Sarah', 'Müller', 'sarah.mueller@techstart.de', '+49 30 111222', 'CEO', 'active', true, NOW(), ARRAY['Entscheider', 'VIP']),
('55555555-5555-5555-5555-555555555556', '11111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444', '22222222-2222-2222-2222-222222222222', 'Thomas', 'Weber', 'thomas.weber@techstart.de', '+49 30 111223', 'CTO', 'active', true, NOW(), ARRAY['Technik']),
('55555555-5555-5555-5555-555555555557', '11111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444445', '22222222-2222-2222-2222-222222222223', 'Anna', 'Becker', 'anna.becker@dataflow.de', '+49 89 222333', 'Head of Sales', 'active', true, NOW(), ARRAY['Sales', 'Entscheider']),
('55555555-5555-5555-5555-555555555558', '11111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444446', '22222222-2222-2222-2222-222222222223', 'Michael', 'Fischer', 'michael.fischer@scaleup.de', '+49 40 333444', 'Managing Partner', 'active', true, NOW(), ARRAY['Partner', 'Entscheider']);
-- ============================================
-- DEALS
-- ============================================
INSERT INTO deals (id, org_id, pipeline_id, contact_id, company_id, owner_id, title, value, stage_id, status, expected_close_date, tags) VALUES
('66666666-6666-6666-6666-666666666666', '11111111-1111-1111-1111-111111111111', '33333333-3333-3333-3333-333333333333', '55555555-5555-5555-5555-555555555555', '44444444-4444-4444-4444-444444444444', '22222222-2222-2222-2222-222222222222', 'TechStart CRM Implementation', 25000.00, 'proposal', 'open', '2026-03-15', ARRAY['Enterprise']),
('66666666-6666-6666-6666-666666666667', '11111111-1111-1111-1111-111111111111', '33333333-3333-3333-3333-333333333333', '55555555-5555-5555-5555-555555555557', '44444444-4444-4444-4444-444444444445', '22222222-2222-2222-2222-222222222223', 'DataFlow Integration Projekt', 15000.00, 'qualified', 'open', '2026-04-01', ARRAY['Integration']),
('66666666-6666-6666-6666-666666666668', '11111111-1111-1111-1111-111111111111', '33333333-3333-3333-3333-333333333333', '55555555-5555-5555-5555-555555555558', '44444444-4444-4444-4444-444444444446', '22222222-2222-2222-2222-222222222222', 'ScaleUp Consulting Deal', 50000.00, 'negotiation', 'open', '2026-02-28', ARRAY['Beratung', 'High Value']);
-- ============================================
-- ACTIVITIES
-- ============================================
INSERT INTO activities (id, org_id, user_id, contact_id, deal_id, type, subject, description, due_date, is_completed) VALUES
('77777777-7777-7777-7777-777777777777', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555', '66666666-6666-6666-6666-666666666666', 'call', 'Erstgespräch', 'Sehr interessiert an CRM Lösung. Termin für Demo vereinbart.', NULL, true),
('77777777-7777-7777-7777-777777777778', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555', '66666666-6666-6666-6666-666666666666', 'meeting', 'Demo Präsentation', 'Demo für das ganze Team zeigen', '2026-02-15 14:00:00', false),
('77777777-7777-7777-7777-777777777779', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222223', '55555555-5555-5555-5555-555555555557', '66666666-6666-6666-6666-666666666667', 'note', 'Anforderungen', 'Benötigt: API Integration mit bestehendem ERP System', NULL, true),
('77777777-7777-7777-7777-777777777780', '11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555558', '66666666-6666-6666-6666-666666666668', 'task', 'Angebot erstellen', 'Individuelles Angebot mit Mengenrabatt', '2026-02-12 17:00:00', false);
-- ============================================
-- AUDIT LOG EXAMPLE
-- ============================================
INSERT INTO audit_logs (org_id, user_id, action, entity_type, entity_id, changes, ip_address) VALUES
('11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', 'create', 'contact', '55555555-5555-5555-5555-555555555555', '{"after": {"first_name": "Sarah", "last_name": "Müller"}}', '192.168.1.1');