diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9580d63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Environment +.env +.env.local +.env.* + +# Backups +*.bak +*.backup +*.dump + +# Logs +*.log + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Temp +tmp/ +temp/ diff --git a/schemas/001_initial_schema.sql b/schemas/001_initial_schema.sql new file mode 100644 index 0000000..44a365d --- /dev/null +++ b/schemas/001_initial_schema.sql @@ -0,0 +1,472 @@ +-- ================================================================ +-- PMC Database Schema - Initial Setup +-- Version: 1.0.0 +-- Date: 2025-12-08 +-- ================================================================ + +-- Extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ================================================================ +-- SCHEMAS +-- ================================================================ + +-- Auth schema: usuarios, tenants, sesiones +CREATE SCHEMA IF NOT EXISTS auth; + +-- CRM schema: clientes, marcas, productos +CREATE SCHEMA IF NOT EXISTS crm; + +-- Projects schema: proyectos, campanas, briefs +CREATE SCHEMA IF NOT EXISTS projects; + +-- Generation schema: jobs, workflows, modelos +CREATE SCHEMA IF NOT EXISTS generation; + +-- Assets schema: biblioteca de archivos +CREATE SCHEMA IF NOT EXISTS assets; + +-- ================================================================ +-- AUTH SCHEMA +-- ================================================================ + +-- Planes de tenant +CREATE TABLE auth.tenant_plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL UNIQUE, + code VARCHAR(50) NOT NULL UNIQUE, + description TEXT, + price_monthly DECIMAL(10,2) DEFAULT 0, + price_yearly DECIMAL(10,2) DEFAULT 0, + max_users INT DEFAULT 5, + max_clients INT DEFAULT 10, + max_brands INT DEFAULT 20, + max_generations_month INT DEFAULT 100, + max_storage_bytes BIGINT DEFAULT 5368709120, -- 5GB + max_custom_models INT DEFAULT 0, + max_training_month INT DEFAULT 0, + features JSONB, + is_active BOOLEAN DEFAULT true, + sort_order INT DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Tenants (organizaciones) +CREATE TABLE auth.tenants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL UNIQUE, + plan_id UUID NOT NULL REFERENCES auth.tenant_plans(id), + status VARCHAR(20) DEFAULT 'trial' CHECK (status IN ('trial', 'active', 'suspended', 'cancelled')), + logo_url VARCHAR(500), + settings JSONB, + trial_ends_at DATE, + billing_cycle_start DATE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_tenants_slug ON auth.tenants(slug); +CREATE INDEX idx_tenants_status ON auth.tenants(status); + +-- Usuarios +CREATE TABLE auth.users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + email VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + avatar_url VARCHAR(500), + role VARCHAR(20) DEFAULT 'viewer' CHECK (role IN ('owner', 'admin', 'creative', 'analyst', 'viewer', 'client_portal')), + status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended')), + last_login_at TIMESTAMPTZ, + preferences JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, email) +); + +CREATE INDEX idx_users_tenant ON auth.users(tenant_id); +CREATE INDEX idx_users_email ON auth.users(email); +CREATE INDEX idx_users_status ON auth.users(status); + +-- Sesiones +CREATE TABLE auth.sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + refresh_token_hash VARCHAR(500) NOT NULL, + device_info VARCHAR(100), + ip_address VARCHAR(50), + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + last_used_at TIMESTAMPTZ, + is_active BOOLEAN DEFAULT true +); + +CREATE INDEX idx_sessions_user ON auth.sessions(user_id); +CREATE INDEX idx_sessions_expires ON auth.sessions(expires_at); + +-- Invitaciones +CREATE TABLE auth.invitations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + email VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL, + token VARCHAR(100) NOT NULL UNIQUE, + invited_by UUID REFERENCES auth.users(id), + expires_at TIMESTAMPTZ NOT NULL, + accepted_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_invitations_token ON auth.invitations(token); +CREATE INDEX idx_invitations_tenant ON auth.invitations(tenant_id); + +-- ================================================================ +-- CRM SCHEMA +-- ================================================================ + +-- Clientes +CREATE TABLE crm.clients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + type VARCHAR(20) DEFAULT 'company' CHECK (type IN ('company', 'individual')), + industry VARCHAR(100), + website VARCHAR(500), + logo_url VARCHAR(500), + contact_name VARCHAR(200), + contact_email VARCHAR(255), + contact_phone VARCHAR(50), + notes TEXT, + is_active BOOLEAN DEFAULT true, + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +CREATE INDEX idx_clients_tenant ON crm.clients(tenant_id); +CREATE INDEX idx_clients_slug ON crm.clients(tenant_id, slug); + +-- Marcas +CREATE TABLE crm.brands ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + client_id UUID NOT NULL REFERENCES crm.clients(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + description TEXT, + logo_url VARCHAR(500), + primary_color VARCHAR(7), + secondary_color VARCHAR(7), + brand_voice TEXT, + target_audience TEXT, + guidelines_url VARCHAR(500), + is_active BOOLEAN DEFAULT true, + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +CREATE INDEX idx_brands_tenant ON crm.brands(tenant_id); +CREATE INDEX idx_brands_client ON crm.brands(client_id); + +-- Productos +CREATE TABLE crm.products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + brand_id UUID NOT NULL REFERENCES crm.brands(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + description TEXT, + sku VARCHAR(100), + category VARCHAR(100), + price DECIMAL(10,2), + currency VARCHAR(3) DEFAULT 'USD', + image_urls JSONB, -- Array of image URLs + attributes JSONB, -- Custom attributes + is_active BOOLEAN DEFAULT true, + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +CREATE INDEX idx_products_tenant ON crm.products(tenant_id); +CREATE INDEX idx_products_brand ON crm.products(brand_id); + +-- ================================================================ +-- PROJECTS SCHEMA +-- ================================================================ + +-- Proyectos +CREATE TABLE projects.projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + client_id UUID REFERENCES crm.clients(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + description TEXT, + status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'paused', 'completed', 'archived')), + start_date DATE, + end_date DATE, + budget DECIMAL(12,2), + currency VARCHAR(3) DEFAULT 'USD', + metadata JSONB, + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +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); + +-- Campanas +CREATE TABLE projects.campaigns ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, + brand_id UUID REFERENCES crm.brands(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + description TEXT, + objective TEXT, + target_audience TEXT, + channels JSONB, -- Array of channels: instagram, facebook, etc + status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'planning', 'in_progress', 'review', 'approved', 'published')), + start_date DATE, + end_date DATE, + brief JSONB, -- Campaign brief details + metadata JSONB, + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +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); + +-- ================================================================ +-- GENERATION SCHEMA +-- ================================================================ + +-- Workflows de generacion +CREATE TABLE generation.workflows ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + code VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + type VARCHAR(50) NOT NULL, -- image, text, composite + input_schema JSONB NOT NULL, -- JSON Schema for inputs + output_schema JSONB, -- Expected outputs + comfyui_template JSONB, -- ComfyUI workflow JSON + estimated_time_seconds INT DEFAULT 60, + is_active BOOLEAN DEFAULT true, + is_system BOOLEAN DEFAULT true, -- System vs custom workflow + tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL for system workflows + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_workflows_code ON generation.workflows(code); +CREATE INDEX idx_workflows_tenant ON generation.workflows(tenant_id); + +-- Jobs de generacion +CREATE TABLE generation.jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id), + workflow_id UUID NOT NULL REFERENCES generation.workflows(id), + campaign_id UUID REFERENCES projects.campaigns(id) ON DELETE SET NULL, + brand_id UUID REFERENCES crm.brands(id) ON DELETE SET NULL, + status VARCHAR(20) DEFAULT 'queued' CHECK (status IN ('queued', 'processing', 'completed', 'failed', 'cancelled')), + priority INT DEFAULT 0, + input_params JSONB NOT NULL, + output_data JSONB, -- Results after completion + error_message TEXT, + progress INT DEFAULT 0, -- 0-100 + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + processing_time_ms INT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_jobs_tenant ON generation.jobs(tenant_id); +CREATE INDEX idx_jobs_user ON generation.jobs(user_id); +CREATE INDEX idx_jobs_status ON generation.jobs(status); +CREATE INDEX idx_jobs_created ON generation.jobs(created_at DESC); + +-- Modelos personalizados (LoRAs) +CREATE TABLE generation.custom_models ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + brand_id UUID REFERENCES crm.brands(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, -- lora, checkpoint, embedding + file_path VARCHAR(500) NOT NULL, + file_size_bytes BIGINT, + trigger_word VARCHAR(100), + training_images_count INT, + preview_images JSONB, -- Array of preview image URLs + status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'training', 'ready', 'failed')), + training_config JSONB, + metadata JSONB, + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_custom_models_tenant ON generation.custom_models(tenant_id); +CREATE INDEX idx_custom_models_brand ON generation.custom_models(brand_id); + +-- ================================================================ +-- ASSETS SCHEMA +-- ================================================================ + +-- Colecciones +CREATE TABLE assets.collections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) NOT NULL, + description TEXT, + is_public BOOLEAN DEFAULT false, + parent_id UUID REFERENCES assets.collections(id) ON DELETE SET NULL, + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE(tenant_id, slug) +); + +CREATE INDEX idx_collections_tenant ON assets.collections(tenant_id); +CREATE INDEX idx_collections_parent ON assets.collections(parent_id); + +-- Assets (archivos) +CREATE TABLE assets.assets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + collection_id UUID REFERENCES assets.collections(id) ON DELETE SET NULL, + job_id UUID REFERENCES generation.jobs(id) ON DELETE SET NULL, -- Si fue generado + name VARCHAR(255) NOT NULL, + original_filename VARCHAR(255), + mime_type VARCHAR(100) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size_bytes BIGINT NOT NULL, + width INT, + height INT, + duration_seconds DECIMAL(10,2), -- For video/audio + thumbnail_url VARCHAR(500), + metadata JSONB, -- EXIF, AI generation params, etc + tags JSONB, -- Array of tags + is_public BOOLEAN DEFAULT false, + version INT DEFAULT 1, + parent_id UUID REFERENCES assets.assets(id) ON DELETE SET NULL, -- For versions + created_by UUID REFERENCES auth.users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_assets_tenant ON assets.assets(tenant_id); +CREATE INDEX idx_assets_collection ON assets.assets(collection_id); +CREATE INDEX idx_assets_job ON assets.assets(job_id); +CREATE INDEX idx_assets_mime ON assets.assets(mime_type); + +-- ================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ================================================================ + +-- Enable RLS on all tenant-scoped tables +ALTER TABLE auth.tenants ENABLE ROW LEVEL SECURITY; +ALTER TABLE auth.users ENABLE ROW LEVEL SECURITY; +ALTER TABLE auth.sessions ENABLE ROW LEVEL SECURITY; +ALTER TABLE auth.invitations ENABLE ROW LEVEL SECURITY; +ALTER TABLE crm.clients ENABLE ROW LEVEL SECURITY; +ALTER TABLE crm.brands ENABLE ROW LEVEL SECURITY; +ALTER TABLE crm.products ENABLE ROW LEVEL SECURITY; +ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; +ALTER TABLE projects.campaigns ENABLE ROW LEVEL SECURITY; +ALTER TABLE generation.jobs ENABLE ROW LEVEL SECURITY; +ALTER TABLE generation.custom_models ENABLE ROW LEVEL SECURITY; +ALTER TABLE assets.collections ENABLE ROW LEVEL SECURITY; +ALTER TABLE assets.assets ENABLE ROW LEVEL SECURITY; + +-- Function to get current tenant from session variable +CREATE OR REPLACE FUNCTION auth.current_tenant_id() +RETURNS UUID AS $$ +BEGIN + RETURN NULLIF(current_setting('app.current_tenant', true), '')::UUID; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- RLS Policies (example for clients, apply similar to all tables) +CREATE POLICY tenant_isolation_policy ON crm.clients + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON crm.brands + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON crm.products + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON projects.projects + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON projects.campaigns + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON generation.jobs + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON generation.custom_models + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON assets.collections + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +CREATE POLICY tenant_isolation_policy ON assets.assets + FOR ALL + USING (tenant_id = auth.current_tenant_id()); + +-- ================================================================ +-- COMMENTS +-- ================================================================ + +COMMENT ON SCHEMA auth IS 'Authentication and authorization: users, tenants, sessions'; +COMMENT ON SCHEMA crm IS 'Customer relationship management: clients, brands, products'; +COMMENT ON SCHEMA projects IS 'Project management: projects, campaigns'; +COMMENT ON SCHEMA generation IS 'AI generation: workflows, jobs, models'; +COMMENT ON SCHEMA assets IS 'Digital asset management: files, collections'; + +COMMENT ON TABLE auth.tenants IS 'Organizations/companies using the platform'; +COMMENT ON TABLE auth.users IS 'Platform users, scoped to tenants'; +COMMENT ON TABLE generation.workflows IS 'AI generation workflow definitions'; +COMMENT ON TABLE generation.jobs IS 'AI generation job queue and history'; diff --git a/seeds/001_initial_data.sql b/seeds/001_initial_data.sql new file mode 100644 index 0000000..bde38d9 --- /dev/null +++ b/seeds/001_initial_data.sql @@ -0,0 +1,274 @@ +-- ================================================================ +-- PMC Database Seeds - Initial Data +-- Version: 1.0.0 +-- Date: 2025-12-08 +-- ================================================================ + +-- ================================================================ +-- TENANT PLANS +-- ================================================================ + +INSERT INTO auth.tenant_plans (id, name, code, description, price_monthly, price_yearly, max_users, max_clients, max_brands, max_generations_month, max_storage_bytes, max_custom_models, max_training_month, features, sort_order) VALUES +-- Starter (Free/Trial) +( + 'a0000000-0000-0000-0000-000000000001', + 'Starter', + 'starter', + 'Plan gratuito para empezar', + 0, + 0, + 3, -- max_users + 5, -- max_clients + 10, -- max_brands + 100, -- max_generations_month + 1073741824, -- max_storage_bytes (1GB) + 0, -- max_custom_models + 0, -- max_training_month + '{"analytics": false, "automation": false, "api_access": false, "priority_support": false}'::JSONB, + 1 +), +-- Pro +( + 'a0000000-0000-0000-0000-000000000002', + 'Pro', + 'pro', + 'Para equipos en crecimiento', + 49.00, + 470.00, + 10, -- max_users + 25, -- max_clients + 50, -- max_brands + 1000, -- max_generations_month + 10737418240, -- max_storage_bytes (10GB) + 10, -- max_custom_models + 5, -- max_training_month + '{"analytics": true, "automation": true, "api_access": false, "priority_support": false}'::JSONB, + 2 +), +-- Business +( + 'a0000000-0000-0000-0000-000000000003', + 'Business', + 'business', + 'Para agencias y empresas', + 149.00, + 1430.00, + 50, -- max_users + 100, -- max_clients + 200, -- max_brands + 10000, -- max_generations_month + 107374182400, -- max_storage_bytes (100GB) + 50, -- max_custom_models + 20, -- max_training_month + '{"analytics": true, "automation": true, "api_access": true, "priority_support": true}'::JSONB, + 3 +), +-- Enterprise +( + 'a0000000-0000-0000-0000-000000000004', + 'Enterprise', + 'enterprise', + 'Plan personalizado para grandes organizaciones', + 0, -- Custom pricing + 0, + -1, -- unlimited (represented as -1) + -1, + -1, + -1, + -1, + -1, + -1, + '{"analytics": true, "automation": true, "api_access": true, "priority_support": true, "sla": true, "custom_integrations": true}'::JSONB, + 4 +); + +-- ================================================================ +-- SYSTEM WORKFLOWS +-- ================================================================ + +INSERT INTO generation.workflows (id, name, code, description, type, input_schema, output_schema, estimated_time_seconds, is_active, is_system, tenant_id) VALUES +-- Product Photo Synthetic +( + 'w0000000-0000-0000-0000-000000000001', + 'Product Photo Synthetic', + 'product_photo_synthetic', + 'Genera fotos de producto en contexto comercial', + 'image', + '{ + "type": "object", + "required": ["product_description"], + "properties": { + "product_description": {"type": "string", "description": "Descripcion del producto"}, + "reference_image": {"type": "string", "format": "uri", "description": "URL imagen referencia"}, + "background": {"type": "string", "enum": ["white", "lifestyle", "custom"], "default": "white"}, + "style": {"type": "string", "enum": ["minimalist", "premium", "casual"], "default": "minimalist"}, + "lora_id": {"type": "string", "format": "uuid", "description": "ID del LoRA de marca"}, + "seed": {"type": "integer", "description": "Seed para reproducibilidad"} + } + }'::JSONB, + '{ + "type": "object", + "properties": { + "images": {"type": "array", "items": {"type": "string", "format": "uri"}}, + "count": {"type": "integer"} + } + }'::JSONB, + 60, + true, + true, + NULL +), +-- Social Media Post +( + 'w0000000-0000-0000-0000-000000000002', + 'Social Media Post', + 'social_media_post', + 'Genera imagen + copy para redes sociales', + 'composite', + '{ + "type": "object", + "required": ["brief", "brand_id"], + "properties": { + "brief": { + "type": "object", + "properties": { + "objetivo": {"type": "string"}, + "audiencia": {"type": "string"}, + "tono": {"type": "string"} + } + }, + "product_id": {"type": "string", "format": "uuid"}, + "channel": {"type": "string", "enum": ["instagram", "facebook", "linkedin", "tiktok"]}, + "format": {"type": "string", "enum": ["post", "story", "carousel"]}, + "brand_id": {"type": "string", "format": "uuid"} + } + }'::JSONB, + '{ + "type": "object", + "properties": { + "images": {"type": "array"}, + "copy": {"type": "string"}, + "hashtags": {"type": "array", "items": {"type": "string"}} + } + }'::JSONB, + 90, + true, + true, + NULL +), +-- Ad Variations +( + 'w0000000-0000-0000-0000-000000000003', + 'Ad Variations', + 'ad_variations', + 'Genera variaciones para A/B testing', + 'image', + '{ + "type": "object", + "required": ["base_image"], + "properties": { + "base_image": {"type": "string", "description": "Asset ID o URL de imagen base"}, + "variations_count": {"type": "integer", "minimum": 2, "maximum": 10, "default": 5}, + "variation_type": {"type": "string", "enum": ["color", "background", "composition"]} + } + }'::JSONB, + '{ + "type": "object", + "properties": { + "variations": {"type": "array"}, + "metadata": {"type": "object"} + } + }'::JSONB, + 120, + true, + true, + NULL +), +-- Virtual Avatar +( + 'w0000000-0000-0000-0000-000000000004', + 'Virtual Avatar', + 'virtual_avatar', + 'Genera avatar/influencer virtual consistente', + 'image', + '{ + "type": "object", + "required": ["character_lora_id"], + "properties": { + "character_lora_id": {"type": "string", "format": "uuid"}, + "pose": {"type": "string", "enum": ["standing", "sitting", "walking", "custom"]}, + "outfit": {"type": "string"}, + "background": {"type": "string"}, + "expression": {"type": "string", "enum": ["happy", "serious", "surprised", "neutral"]} + } + }'::JSONB, + '{ + "type": "object", + "properties": { + "images": {"type": "array"}, + "character_consistency_score": {"type": "number"} + } + }'::JSONB, + 90, + true, + true, + NULL +); + +-- ================================================================ +-- DEMO TENANT (for development) +-- ================================================================ + +-- Only insert demo data in development +DO $$ +BEGIN + -- Demo Tenant + INSERT INTO auth.tenants (id, name, slug, plan_id, status, trial_ends_at) VALUES + ( + 'd0000000-0000-0000-0000-000000000001', + 'Demo Agency', + 'demo-agency', + 'a0000000-0000-0000-0000-000000000002', -- Pro plan + 'active', + (CURRENT_DATE + INTERVAL '30 days')::DATE + ); + + -- Demo User (password: demo123456) + INSERT INTO auth.users (id, tenant_id, email, password_hash, first_name, last_name, role, status) VALUES + ( + 'u0000000-0000-0000-0000-000000000001', + 'd0000000-0000-0000-0000-000000000001', + 'demo@pmc.dev', + '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.E5F5u5V5V5V5V5V', -- bcrypt hash of 'demo123456' + 'Demo', + 'User', + 'owner', + 'active' + ); + + -- Demo Client + INSERT INTO crm.clients (id, tenant_id, name, slug, type, industry) VALUES + ( + 'c0000000-0000-0000-0000-000000000001', + 'd0000000-0000-0000-0000-000000000001', + 'Acme Corporation', + 'acme-corp', + 'company', + 'Technology' + ); + + -- Demo Brand + INSERT INTO crm.brands (id, tenant_id, client_id, name, slug, primary_color, secondary_color) VALUES + ( + 'b0000000-0000-0000-0000-000000000001', + 'd0000000-0000-0000-0000-000000000001', + 'c0000000-0000-0000-0000-000000000001', + 'Acme Brand', + 'acme-brand', + '#3B82F6', + '#10B981' + ); + +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Demo data already exists or error: %', SQLERRM; +END $$;