---
id: "ESQUEMA-BD"
title: "Esquema de Base de Datos - Platform Marketing Content"
type: "Model"
status: "Draft"
project: "platform_marketing_content"
version: "1.0.0"
created_date: "2026-01-04"
updated_date: "2026-01-04"
---
# Esquema de Base de Datos - Platform Marketing Content

**Versión:** 1.0.0
**Fecha:** 2025-12-08
**Motor:** PostgreSQL 15+

---

## Configuración General

```sql
-- Extensiones requeridas
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Schemas por módulo
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS crm;
CREATE SCHEMA IF NOT EXISTS projects;
CREATE SCHEMA IF NOT EXISTS generation;
CREATE SCHEMA IF NOT EXISTS assets;
CREATE SCHEMA IF NOT EXISTS automation;
CREATE SCHEMA IF NOT EXISTS analytics;
```

---

## Schema: auth

### Tabla: plans

```sql
CREATE TABLE auth.plans (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(100) NOT NULL,
    code VARCHAR(50) NOT NULL UNIQUE,
    features JSONB NOT NULL DEFAULT '{}',
    limits JSONB NOT NULL DEFAULT '{}',
    price_monthly DECIMAL(10,2) DEFAULT 0,
    price_yearly DECIMAL(10,2) DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE auth.plans IS 'Planes de suscripción con límites';

-- Índices
CREATE INDEX idx_plans_code ON auth.plans(code);
CREATE INDEX idx_plans_active ON auth.plans(is_active);
```

### Tabla: tenants

```sql
CREATE TABLE auth.tenants (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    status VARCHAR(20) NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'suspended', 'trial', 'cancelled')),
    plan_id UUID NOT NULL REFERENCES auth.plans(id),
    settings JSONB NOT NULL DEFAULT '{}',
    branding JSONB NOT NULL DEFAULT '{}',
    limits JSONB DEFAULT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL
);

COMMENT ON TABLE auth.tenants IS 'Organizaciones/agencias que usan la plataforma';

-- Índices
CREATE INDEX idx_tenants_slug ON auth.tenants(slug);
CREATE INDEX idx_tenants_status ON auth.tenants(status);
CREATE INDEX idx_tenants_plan ON auth.tenants(plan_id);
```

### Tabla: roles

```sql
CREATE TABLE auth.roles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID REFERENCES auth.tenants(id),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    permissions TEXT[] NOT NULL DEFAULT '{}',
    is_system BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE auth.roles IS 'Roles con permisos asignados';

-- Índices
CREATE INDEX idx_roles_tenant ON auth.roles(tenant_id);
CREATE UNIQUE INDEX idx_roles_name_tenant ON auth.roles(tenant_id, name);
```

### Tabla: users

```sql
CREATE TABLE auth.users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    avatar_url VARCHAR(500),
    status VARCHAR(20) NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'active', 'suspended', 'deactivated')),
    role_id UUID NOT NULL REFERENCES auth.roles(id),
    preferences JSONB NOT NULL DEFAULT '{}',
    last_login_at TIMESTAMP WITH TIME ZONE,
    email_verified_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(tenant_id, email)
);

COMMENT ON TABLE auth.users IS 'Usuarios del sistema';

-- Índices
CREATE INDEX idx_users_tenant ON auth.users(tenant_id);
CREATE INDEX idx_users_email ON auth.users(email);
CREATE INDEX idx_users_role ON auth.users(role_id);
CREATE INDEX idx_users_status ON auth.users(status);
```

### Tabla: invitations

```sql
CREATE TABLE auth.invitations (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    email VARCHAR(255) NOT NULL,
    role_id UUID NOT NULL REFERENCES auth.roles(id),
    invited_by UUID NOT NULL REFERENCES auth.users(id),
    token VARCHAR(255) NOT NULL UNIQUE,
    status VARCHAR(20) NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'accepted', 'expired', 'cancelled')),
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    accepted_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE auth.invitations IS 'Invitaciones pendientes';

-- Índices
CREATE INDEX idx_invitations_tenant ON auth.invitations(tenant_id);
CREATE INDEX idx_invitations_token ON auth.invitations(token);
CREATE INDEX idx_invitations_status ON auth.invitations(status);
```

### Tabla: audit_logs

```sql
CREATE TABLE auth.audit_logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    user_id UUID REFERENCES auth.users(id),
    action VARCHAR(100) NOT NULL,
    entity_type VARCHAR(100),
    entity_id UUID,
    old_values JSONB,
    new_values JSONB,
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE auth.audit_logs IS 'Registro de auditoría inmutable';

-- Índices
CREATE INDEX idx_audit_tenant ON auth.audit_logs(tenant_id);
CREATE INDEX idx_audit_user ON auth.audit_logs(user_id);
CREATE INDEX idx_audit_action ON auth.audit_logs(action);
CREATE INDEX idx_audit_created ON auth.audit_logs(created_at);
```

### Tabla: settings

```sql
CREATE TABLE auth.settings (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID REFERENCES auth.tenants(id),
    key VARCHAR(255) NOT NULL,
    value JSONB NOT NULL,
    type VARCHAR(20) NOT NULL DEFAULT 'string'
        CHECK (type IN ('string', 'number', 'boolean', 'json')),
    category VARCHAR(100),
    is_secret BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(tenant_id, key)
);

COMMENT ON TABLE auth.settings IS 'Configuración del sistema';

-- Índices
CREATE INDEX idx_settings_tenant ON auth.settings(tenant_id);
CREATE INDEX idx_settings_category ON auth.settings(category);
```

---

## Schema: crm

### Tabla: clients

```sql
CREATE TABLE crm.clients (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    legal_name VARCHAR(255),
    tax_id VARCHAR(50),
    industry VARCHAR(100),
    size VARCHAR(20) CHECK (size IN ('micro', 'small', 'medium', 'large', 'enterprise')),
    website VARCHAR(500),
    status VARCHAR(20) NOT NULL DEFAULT 'prospect'
        CHECK (status IN ('prospect', 'active', 'inactive', 'churned')),
    notes TEXT,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_clients_tenant ON crm.clients(tenant_id);
CREATE INDEX idx_clients_status ON crm.clients(status);
CREATE INDEX idx_clients_name ON crm.clients(name);

-- RLS
ALTER TABLE crm.clients ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON crm.clients
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: contacts

```sql
CREATE TABLE crm.contacts (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    client_id UUID NOT NULL REFERENCES crm.clients(id) ON DELETE CASCADE,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL,
    phone VARCHAR(50),
    position VARCHAR(100),
    department VARCHAR(100),
    is_primary BOOLEAN NOT NULL DEFAULT false,
    status VARCHAR(20) NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'inactive')),
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_contacts_tenant ON crm.contacts(tenant_id);
CREATE INDEX idx_contacts_client ON crm.contacts(client_id);
CREATE INDEX idx_contacts_email ON crm.contacts(email);

-- RLS
ALTER TABLE crm.contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON crm.contacts
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: brands

```sql
CREATE TABLE crm.brands (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    client_id UUID NOT NULL REFERENCES crm.clients(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    status VARCHAR(20) NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'inactive')),
    identity JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_brands_tenant ON crm.brands(tenant_id);
CREATE INDEX idx_brands_client ON crm.brands(client_id);

-- RLS
ALTER TABLE crm.brands ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON crm.brands
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: products

```sql
CREATE TABLE crm.products (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    brand_id UUID NOT NULL REFERENCES crm.brands(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    sku VARCHAR(100),
    description TEXT,
    category VARCHAR(100),
    status VARCHAR(20) NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'discontinued', 'coming_soon')),
    attributes JSONB DEFAULT '{}',
    reference_images TEXT[] DEFAULT '{}',
    lora_model_id UUID,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_products_tenant ON crm.products(tenant_id);
CREATE INDEX idx_products_brand ON crm.products(brand_id);
CREATE INDEX idx_products_sku ON crm.products(sku);

-- RLS
ALTER TABLE crm.products ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON crm.products
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: opportunities

```sql
CREATE TABLE crm.opportunities (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    client_id UUID NOT NULL REFERENCES crm.clients(id),
    contact_id UUID REFERENCES crm.contacts(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    value DECIMAL(15,2),
    currency VARCHAR(3) DEFAULT 'USD',
    stage VARCHAR(20) NOT NULL DEFAULT 'lead'
        CHECK (stage IN ('lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost')),
    probability INTEGER DEFAULT 0 CHECK (probability >= 0 AND probability <= 100),
    expected_close_date DATE,
    actual_close_date DATE,
    lost_reason TEXT,
    notes TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_opportunities_tenant ON crm.opportunities(tenant_id);
CREATE INDEX idx_opportunities_client ON crm.opportunities(client_id);
CREATE INDEX idx_opportunities_stage ON crm.opportunities(stage);

-- RLS
ALTER TABLE crm.opportunities ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON crm.opportunities
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

---

## Schema: projects

### Tabla: projects

```sql
CREATE TABLE projects.projects (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    client_id UUID NOT NULL REFERENCES crm.clients(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    code VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'draft'
        CHECK (status IN ('draft', 'active', 'on_hold', 'completed', 'cancelled')),
    start_date DATE,
    end_date DATE,
    budget DECIMAL(15,2),
    currency VARCHAR(3) DEFAULT 'USD',
    owner_id UUID NOT NULL REFERENCES auth.users(id),
    team_members UUID[] DEFAULT '{}',
    settings JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(tenant_id, code)
);

-- Índices
CREATE INDEX idx_projects_tenant ON projects.projects(tenant_id);
CREATE INDEX idx_projects_client ON projects.projects(client_id);
CREATE INDEX idx_projects_status ON projects.projects(status);
CREATE INDEX idx_projects_owner ON projects.projects(owner_id);

-- RLS
ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects.projects
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: campaigns

```sql
CREATE TABLE projects.campaigns (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE,
    brand_id UUID NOT NULL REFERENCES crm.brands(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    type VARCHAR(50) NOT NULL
        CHECK (type IN ('social_media', 'performance_ads', 'catalog', 'landing', 'email', 'other')),
    status VARCHAR(20) NOT NULL DEFAULT 'draft'
        CHECK (status IN ('draft', 'briefing', 'in_production', 'review', 'approved', 'published', 'archived')),
    brief JSONB NOT NULL DEFAULT '{}',
    channels TEXT[] DEFAULT '{}',
    start_date DATE,
    end_date DATE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_campaigns_tenant ON projects.campaigns(tenant_id);
CREATE INDEX idx_campaigns_project ON projects.campaigns(project_id);
CREATE INDEX idx_campaigns_brand ON projects.campaigns(brand_id);
CREATE INDEX idx_campaigns_status ON projects.campaigns(status);

-- RLS
ALTER TABLE projects.campaigns ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects.campaigns
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: campaign_assets

```sql
CREATE TABLE projects.campaign_assets (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    campaign_id UUID NOT NULL REFERENCES projects.campaigns(id) ON DELETE CASCADE,
    asset_id UUID NOT NULL REFERENCES assets.assets(id) ON DELETE CASCADE,
    status VARCHAR(20) NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'approved', 'rejected', 'revision_requested')),
    feedback TEXT,
    approved_by UUID REFERENCES auth.users(id),
    approved_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(campaign_id, asset_id)
);

-- Índices
CREATE INDEX idx_campaign_assets_campaign ON projects.campaign_assets(campaign_id);
CREATE INDEX idx_campaign_assets_asset ON projects.campaign_assets(asset_id);
CREATE INDEX idx_campaign_assets_status ON projects.campaign_assets(status);
```

---

## Schema: generation

### Tabla: workflow_templates

```sql
CREATE TABLE generation.workflow_templates (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    type VARCHAR(50) NOT NULL
        CHECK (type IN ('product_photo', 'social_post', 'banner', 'avatar', 'variation', 'custom')),
    category VARCHAR(100),
    comfyui_workflow JSONB NOT NULL,
    input_schema JSONB NOT NULL,
    output_config JSONB NOT NULL DEFAULT '{}',
    models_required TEXT[] DEFAULT '{}',
    estimated_time_seconds INTEGER DEFAULT 60,
    is_active BOOLEAN NOT NULL DEFAULT true,
    is_system BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_workflows_tenant ON generation.workflow_templates(tenant_id);
CREATE INDEX idx_workflows_type ON generation.workflow_templates(type);
CREATE INDEX idx_workflows_active ON generation.workflow_templates(is_active);
```

### Tabla: custom_models

```sql
CREATE TABLE generation.custom_models (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    brand_id UUID REFERENCES crm.brands(id),
    name VARCHAR(255) NOT NULL,
    type VARCHAR(20) NOT NULL
        CHECK (type IN ('lora', 'checkpoint', 'embedding', 'controlnet')),
    purpose VARCHAR(100),
    description TEXT,
    file_path VARCHAR(500) NOT NULL,
    file_size BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'ready'
        CHECK (status IN ('training', 'ready', 'failed', 'archived')),
    training_params JSONB DEFAULT '{}',
    trigger_word VARCHAR(100),
    preview_images TEXT[] DEFAULT '{}',
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_models_tenant ON generation.custom_models(tenant_id);
CREATE INDEX idx_models_brand ON generation.custom_models(brand_id);
CREATE INDEX idx_models_type ON generation.custom_models(type);
CREATE INDEX idx_models_status ON generation.custom_models(status);

-- RLS
ALTER TABLE generation.custom_models ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON generation.custom_models
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: generation_jobs

```sql
CREATE TABLE generation.generation_jobs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    campaign_id UUID REFERENCES projects.campaigns(id),
    product_id UUID REFERENCES crm.products(id),
    user_id UUID NOT NULL REFERENCES auth.users(id),
    type VARCHAR(20) NOT NULL
        CHECK (type IN ('image', 'text', 'image_batch', 'mixed')),
    status VARCHAR(20) NOT NULL DEFAULT 'queued'
        CHECK (status IN ('queued', 'processing', 'completed', 'failed', 'cancelled')),
    priority INTEGER NOT NULL DEFAULT 5 CHECK (priority >= 1 AND priority <= 10),
    workflow_id UUID REFERENCES generation.workflow_templates(id),
    input_params JSONB NOT NULL,
    output_assets UUID[] DEFAULT '{}',
    error_message TEXT,
    progress INTEGER DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
    started_at TIMESTAMP WITH TIME ZONE,
    completed_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_jobs_tenant ON generation.generation_jobs(tenant_id);
CREATE INDEX idx_jobs_campaign ON generation.generation_jobs(campaign_id);
CREATE INDEX idx_jobs_user ON generation.generation_jobs(user_id);
CREATE INDEX idx_jobs_status ON generation.generation_jobs(status);
CREATE INDEX idx_jobs_priority ON generation.generation_jobs(priority DESC);
CREATE INDEX idx_jobs_created ON generation.generation_jobs(created_at);

-- RLS
ALTER TABLE generation.generation_jobs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON generation.generation_jobs
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: text_generations

```sql
CREATE TABLE generation.text_generations (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    job_id UUID REFERENCES generation.generation_jobs(id),
    campaign_id UUID REFERENCES projects.campaigns(id),
    type VARCHAR(20) NOT NULL
        CHECK (type IN ('copy', 'title', 'description', 'hashtags', 'cta', 'full_post')),
    prompt TEXT NOT NULL,
    context JSONB NOT NULL DEFAULT '{}',
    output TEXT,
    variations TEXT[] DEFAULT '{}',
    model_used VARCHAR(100),
    tokens_used INTEGER,
    status VARCHAR(20) NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'completed', 'failed')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_text_gen_tenant ON generation.text_generations(tenant_id);
CREATE INDEX idx_text_gen_job ON generation.text_generations(job_id);
CREATE INDEX idx_text_gen_campaign ON generation.text_generations(campaign_id);

-- RLS
ALTER TABLE generation.text_generations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON generation.text_generations
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

---

## Schema: assets

### Tabla: assets

```sql
CREATE TABLE assets.assets (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    type VARCHAR(20) NOT NULL
        CHECK (type IN ('image', 'copy', 'video', 'document', 'model', 'template')),
    mime_type VARCHAR(100),
    file_path VARCHAR(500) NOT NULL,
    file_size BIGINT NOT NULL,
    dimensions JSONB,
    duration INTEGER,
    status VARCHAR(20) NOT NULL DEFAULT 'draft'
        CHECK (status IN ('draft', 'pending_review', 'approved', 'rejected', 'archived')),
    visibility VARCHAR(20) NOT NULL DEFAULT 'team'
        CHECK (visibility IN ('private', 'team', 'client')),
    source VARCHAR(20) NOT NULL DEFAULT 'uploaded'
        CHECK (source IN ('generated', 'uploaded', 'imported')),
    generation_job_id UUID REFERENCES generation.generation_jobs(id),
    metadata JSONB DEFAULT '{}',
    tags TEXT[] DEFAULT '{}',
    created_by UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP WITH TIME ZONE
);

-- Índices
CREATE INDEX idx_assets_tenant ON assets.assets(tenant_id);
CREATE INDEX idx_assets_type ON assets.assets(type);
CREATE INDEX idx_assets_status ON assets.assets(status);
CREATE INDEX idx_assets_job ON assets.assets(generation_job_id);
CREATE INDEX idx_assets_created ON assets.assets(created_at);
CREATE INDEX idx_assets_tags ON assets.assets USING GIN(tags);
CREATE INDEX idx_assets_name_search ON assets.assets USING GIN(to_tsvector('spanish', name || ' ' || COALESCE(description, '')));

-- RLS
ALTER TABLE assets.assets ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON assets.assets
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: asset_versions

```sql
CREATE TABLE assets.asset_versions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    asset_id UUID NOT NULL REFERENCES assets.assets(id) ON DELETE CASCADE,
    version_number INTEGER NOT NULL,
    file_path VARCHAR(500) NOT NULL,
    file_size BIGINT NOT NULL,
    changes_description TEXT,
    created_by UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(asset_id, version_number)
);

-- Índices
CREATE INDEX idx_versions_asset ON assets.asset_versions(asset_id);
```

### Tabla: collections

```sql
CREATE TABLE assets.collections (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    type VARCHAR(20) NOT NULL DEFAULT 'manual'
        CHECK (type IN ('manual', 'smart', 'campaign', 'brand')),
    smart_filters JSONB,
    cover_asset_id UUID REFERENCES assets.assets(id),
    is_public BOOLEAN NOT NULL DEFAULT false,
    created_by UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_collections_tenant ON assets.collections(tenant_id);

-- RLS
ALTER TABLE assets.collections ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON assets.collections
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: collection_assets

```sql
CREATE TABLE assets.collection_assets (
    collection_id UUID NOT NULL REFERENCES assets.collections(id) ON DELETE CASCADE,
    asset_id UUID NOT NULL REFERENCES assets.assets(id) ON DELETE CASCADE,
    added_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (collection_id, asset_id)
);

-- Índices
CREATE INDEX idx_coll_assets_collection ON assets.collection_assets(collection_id);
CREATE INDEX idx_coll_assets_asset ON assets.collection_assets(asset_id);
```

### Tabla: asset_comments

```sql
CREATE TABLE assets.asset_comments (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    asset_id UUID NOT NULL REFERENCES assets.assets(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES auth.users(id),
    content TEXT NOT NULL,
    position JSONB,
    is_resolved BOOLEAN NOT NULL DEFAULT false,
    parent_id UUID REFERENCES assets.asset_comments(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_comments_asset ON assets.asset_comments(asset_id);
CREATE INDEX idx_comments_user ON assets.asset_comments(user_id);
CREATE INDEX idx_comments_parent ON assets.asset_comments(parent_id);
```

### Tabla: downloads

```sql
CREATE TABLE assets.downloads (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    asset_id UUID REFERENCES assets.assets(id),
    collection_id UUID REFERENCES assets.collections(id),
    user_id UUID NOT NULL REFERENCES auth.users(id),
    download_type VARCHAR(20) NOT NULL
        CHECK (download_type IN ('single', 'batch', 'collection')),
    format VARCHAR(50),
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_downloads_tenant ON assets.downloads(tenant_id);
CREATE INDEX idx_downloads_asset ON assets.downloads(asset_id);
CREATE INDEX idx_downloads_user ON assets.downloads(user_id);
CREATE INDEX idx_downloads_created ON assets.downloads(created_at);

-- RLS
ALTER TABLE assets.downloads ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON assets.downloads
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

---

## Schema: automation

### Tabla: automation_flows

```sql
CREATE TABLE automation.automation_flows (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    type VARCHAR(20) NOT NULL
        CHECK (type IN ('trigger_based', 'scheduled', 'manual')),
    trigger_event VARCHAR(100),
    n8n_workflow_id VARCHAR(100),
    is_active BOOLEAN NOT NULL DEFAULT false,
    last_run TIMESTAMP WITH TIME ZONE,
    run_count INTEGER NOT NULL DEFAULT 0,
    config JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_flows_tenant ON automation.automation_flows(tenant_id);
CREATE INDEX idx_flows_trigger ON automation.automation_flows(trigger_event);
CREATE INDEX idx_flows_active ON automation.automation_flows(is_active);

-- RLS
ALTER TABLE automation.automation_flows ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON automation.automation_flows
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: automation_runs

```sql
CREATE TABLE automation.automation_runs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    flow_id UUID NOT NULL REFERENCES automation.automation_flows(id),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    status VARCHAR(20) NOT NULL DEFAULT 'running'
        CHECK (status IN ('running', 'completed', 'failed', 'cancelled')),
    trigger_data JSONB,
    output_data JSONB,
    error_message TEXT,
    started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMP WITH TIME ZONE,
    duration_ms INTEGER
);

-- Índices
CREATE INDEX idx_runs_flow ON automation.automation_runs(flow_id);
CREATE INDEX idx_runs_tenant ON automation.automation_runs(tenant_id);
CREATE INDEX idx_runs_status ON automation.automation_runs(status);
CREATE INDEX idx_runs_started ON automation.automation_runs(started_at);

-- RLS
ALTER TABLE automation.automation_runs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON automation.automation_runs
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: webhook_endpoints

```sql
CREATE TABLE automation.webhook_endpoints (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) NOT NULL,
    secret_key VARCHAR(255) NOT NULL,
    target_flow_id UUID NOT NULL REFERENCES automation.automation_flows(id),
    is_active BOOLEAN NOT NULL DEFAULT true,
    last_called TIMESTAMP WITH TIME ZONE,
    call_count INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(tenant_id, slug)
);

-- Índices
CREATE INDEX idx_webhooks_tenant ON automation.webhook_endpoints(tenant_id);
CREATE INDEX idx_webhooks_slug ON automation.webhook_endpoints(slug);

-- RLS
ALTER TABLE automation.webhook_endpoints ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON automation.webhook_endpoints
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

---

## Schema: analytics

### Tabla: metrics

```sql
CREATE TABLE analytics.metrics (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    metric_type VARCHAR(100) NOT NULL,
    dimension_1 VARCHAR(255),
    dimension_2 VARCHAR(255),
    value DECIMAL(20,4) NOT NULL,
    period_type VARCHAR(10) NOT NULL
        CHECK (period_type IN ('hour', 'day', 'week', 'month')),
    period_start TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_metrics_tenant_type ON analytics.metrics(tenant_id, metric_type);
CREATE INDEX idx_metrics_period ON analytics.metrics(period_type, period_start);
CREATE INDEX idx_metrics_tenant_period ON analytics.metrics(tenant_id, period_type, period_start);

-- RLS
ALTER TABLE analytics.metrics ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON analytics.metrics
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: reports

```sql
CREATE TABLE analytics.reports (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    type VARCHAR(50) NOT NULL,
    parameters JSONB NOT NULL DEFAULT '{}',
    file_path VARCHAR(500),
    file_format VARCHAR(10) NOT NULL
        CHECK (file_format IN ('pdf', 'xlsx', 'csv')),
    generated_by UUID NOT NULL REFERENCES auth.users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_reports_tenant ON analytics.reports(tenant_id);
CREATE INDEX idx_reports_type ON analytics.reports(type);

-- RLS
ALTER TABLE analytics.reports ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON analytics.reports
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### Tabla: saved_views

```sql
CREATE TABLE analytics.saved_views (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    user_id UUID NOT NULL REFERENCES auth.users(id),
    name VARCHAR(255) NOT NULL,
    dashboard VARCHAR(50) NOT NULL,
    config JSONB NOT NULL,
    is_default BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Índices
CREATE INDEX idx_views_tenant ON analytics.saved_views(tenant_id);
CREATE INDEX idx_views_user ON analytics.saved_views(user_id);

-- RLS
ALTER TABLE analytics.saved_views ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON analytics.saved_views
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

---

## Tabla: brand_models (relación N:N)

```sql
CREATE TABLE generation.brand_models (
    brand_id UUID NOT NULL REFERENCES crm.brands(id) ON DELETE CASCADE,
    model_id UUID NOT NULL REFERENCES generation.custom_models(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (brand_id, model_id)
);

-- Índices
CREATE INDEX idx_brand_models_brand ON generation.brand_models(brand_id);
CREATE INDEX idx_brand_models_model ON generation.brand_models(model_id);
```

---

## Funciones de Utilidad

```sql
-- Función para generar código de proyecto
CREATE OR REPLACE FUNCTION projects.generate_project_code(p_tenant_id UUID)
RETURNS VARCHAR AS $$
DECLARE
    v_year VARCHAR(4);
    v_count INTEGER;
BEGIN
    v_year := TO_CHAR(CURRENT_DATE, 'YYYY');

    SELECT COUNT(*) + 1 INTO v_count
    FROM projects.projects
    WHERE tenant_id = p_tenant_id
      AND code LIKE 'PRJ-' || v_year || '-%';

    RETURN 'PRJ-' || v_year || '-' || LPAD(v_count::TEXT, 3, '0');
END;
$$ LANGUAGE plpgsql;

-- Trigger para auto-generar código de proyecto
CREATE OR REPLACE FUNCTION projects.set_project_code()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.code IS NULL OR NEW.code = '' THEN
        NEW.code := projects.generate_project_code(NEW.tenant_id);
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tr_set_project_code
    BEFORE INSERT ON projects.projects
    FOR EACH ROW EXECUTE FUNCTION projects.set_project_code();

-- Función para actualizar updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Aplicar trigger a todas las tablas con updated_at
-- (Ejemplo para una tabla, repetir para todas)
CREATE TRIGGER tr_update_timestamp
    BEFORE UPDATE ON auth.tenants
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();
```

---

## Referencias

- [MODELO-DOMINIO.md](./MODELO-DOMINIO.md)
- [ARQUITECTURA-TECNICA.md](../00-vision-general/ARQUITECTURA-TECNICA.md)

---

**Documento generado por:** Requirements-Analyst
**Fecha:** 2025-12-08
