diff --git a/docs/ER-DIAGRAM.md b/docs/ER-DIAGRAM.md new file mode 100644 index 0000000..0db4375 --- /dev/null +++ b/docs/ER-DIAGRAM.md @@ -0,0 +1,149 @@ +# Pulse CRM - Entity Relationship Diagram + +```mermaid +erDiagram + ORGANIZATIONS ||--o{ USERS : has + ORGANIZATIONS ||--o{ CONTACTS : has + ORGANIZATIONS ||--o{ COMPANIES : has + ORGANIZATIONS ||--o{ PIPELINES : has + ORGANIZATIONS ||--o{ DEALS : has + ORGANIZATIONS ||--o{ ACTIVITIES : has + ORGANIZATIONS ||--o{ AUDIT_LOGS : has + + USERS ||--o{ CONTACTS : owns + USERS ||--o{ COMPANIES : owns + USERS ||--o{ DEALS : owns + USERS ||--o{ ACTIVITIES : creates + USERS ||--o{ REFRESH_TOKENS : has + + COMPANIES ||--o{ CONTACTS : employs + COMPANIES ||--o{ DEALS : has + + CONTACTS ||--o{ DEALS : associated + CONTACTS ||--o{ ACTIVITIES : has + + PIPELINES ||--o{ DEALS : contains + + DEALS ||--o{ ACTIVITIES : has + + ORGANIZATIONS { + uuid id PK + string name + string slug UK + jsonb settings + string subscription_plan + timestamp created_at + } + + USERS { + uuid id PK + uuid org_id FK + string email UK + string password_hash + string first_name + string last_name + string role + boolean is_active + timestamp last_login_at + } + + CONTACTS { + uuid id PK + uuid org_id FK + uuid company_id FK + uuid owner_id FK + string first_name + string last_name + string email + string phone + string status + boolean gdpr_consent + timestamp deleted_at + } + + COMPANIES { + uuid id PK + uuid org_id FK + uuid owner_id FK + string name + string industry + string website + string phone + timestamp deleted_at + } + + PIPELINES { + uuid id PK + uuid org_id FK + string name + boolean is_default + jsonb stages + } + + DEALS { + uuid id PK + uuid org_id FK + uuid pipeline_id FK + uuid contact_id FK + uuid company_id FK + uuid owner_id FK + string title + decimal value + string stage_id + string status + date expected_close_date + } + + ACTIVITIES { + uuid id PK + uuid org_id FK + uuid user_id FK + uuid contact_id FK + uuid deal_id FK + string type + string subject + text description + timestamp due_date + boolean is_completed + } + + AUDIT_LOGS { + uuid id PK + uuid org_id FK + uuid user_id FK + string action + string entity_type + uuid entity_id + jsonb changes + inet ip_address + } + + REFRESH_TOKENS { + uuid id PK + uuid user_id FK + string token_hash + timestamp expires_at + } +``` + +## Tabellen-Übersicht + +| Tabelle | Beschreibung | Relationships | +|---------|--------------|---------------| +| `organizations` | Mandanten (Multi-Tenancy) | Parent für alle Daten | +| `users` | Benutzer/Team-Mitglieder | Gehört zu Organization | +| `contacts` | Personen/Leads | Gehört zu Organization, optional zu Company | +| `companies` | Firmen | Gehört zu Organization | +| `pipelines` | Sales Pipelines | Gehört zu Organization | +| `deals` | Opportunities | Gehört zu Pipeline, Contact, Company | +| `activities` | Notizen, Calls, Tasks | Gehört zu Contact oder Deal | +| `audit_logs` | DSGVO Audit Trail | Protokolliert alle Änderungen | +| `refresh_tokens` | JWT Refresh Tokens | Gehört zu User | + +## Indizes + +Alle Tabellen haben Indizes auf: +- `org_id` - Multi-Tenancy Queries +- Foreign Keys +- Häufig gefilterte Felder (status, email, etc.) +- `deleted_at` - Soft Delete Queries diff --git a/src/db/schema.sql b/src/db/schema.sql new file mode 100644 index 0000000..0f3c5d4 --- /dev/null +++ b/src/db/schema.sql @@ -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(); diff --git a/src/db/seed.sql b/src/db/seed.sql new file mode 100644 index 0000000..bf9a0c7 --- /dev/null +++ b/src/db/seed.sql @@ -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');