-- ============================================================================ -- SCHEMA: tenants -- TABLE: tenants, tenant_settings -- DESCRIPTION: Sistema de Multi-Tenancy para organizaciones/empresas -- VERSION: 1.0.0 -- CREATED: 2026-01-10 -- ============================================================================ -- Crear schema si no existe CREATE SCHEMA IF NOT EXISTS tenants; -- Enum para estado de tenant DO $$ BEGIN CREATE TYPE tenants.tenant_status AS ENUM ( 'pending', -- Pendiente de activacion 'active', -- Activo y operativo 'suspended', -- Suspendido (impago o violacion) 'deleted' -- Eliminado (soft delete) ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para estado de subscription DO $$ BEGIN CREATE TYPE tenants.subscription_status AS ENUM ( 'trialing', -- En periodo de prueba 'active', -- Activo con pagos al dia 'past_due', -- Pago vencido 'canceled', -- Cancelado 'incomplete' -- Configuracion incompleta ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla principal de Tenants (Organizaciones) CREATE TABLE IF NOT EXISTS tenants.tenants ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Informacion basica name VARCHAR(255) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE, domain VARCHAR(255), logo_url TEXT, -- Estado status tenants.tenant_status NOT NULL DEFAULT 'pending', -- Subscription info (referencia a billing) plan_id UUID, -- FK a plans.plans (se agrega despues) subscription_status tenants.subscription_status DEFAULT 'trialing', trial_ends_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '14 days'), -- Stripe integration stripe_customer_id VARCHAR(255), stripe_subscription_id VARCHAR(255), -- Settings (JSONB flexible) settings JSONB NOT NULL DEFAULT '{ "timezone": "America/Mexico_City", "locale": "es-MX", "currency": "MXN", "date_format": "DD/MM/YYYY" }'::JSONB, -- Metadata adicional metadata JSONB DEFAULT '{}'::JSONB, -- Soft delete deleted_at TIMESTAMPTZ, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE tenants.tenants IS 'Organizaciones/empresas que usan la plataforma. Base del sistema multi-tenant.'; COMMENT ON COLUMN tenants.tenants.slug IS 'Identificador URL-safe unico para el tenant (ej: acme-corp)'; COMMENT ON COLUMN tenants.tenants.settings IS 'Configuracion del tenant: timezone, locale, currency, etc.'; -- Indices CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants.tenants(slug); CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants.tenants(status) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_tenants_stripe_customer ON tenants.tenants(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_tenants_subscription_status ON tenants.tenants(subscription_status); -- Trigger para updated_at CREATE OR REPLACE FUNCTION tenants.update_tenant_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS tenant_updated_at ON tenants.tenants; CREATE TRIGGER tenant_updated_at BEFORE UPDATE ON tenants.tenants FOR EACH ROW EXECUTE FUNCTION tenants.update_tenant_timestamp(); -- ============================================================================ -- TABLA: tenant_settings (configuracion extendida) -- ============================================================================ CREATE TABLE IF NOT EXISTS tenants.tenant_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, -- Configuracion por categoria category VARCHAR(50) NOT NULL, -- 'branding', 'notifications', 'security', etc. key VARCHAR(100) NOT NULL, value JSONB NOT NULL, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Unique por tenant + category + key UNIQUE (tenant_id, category, key) ); COMMENT ON TABLE tenants.tenant_settings IS 'Configuraciones extendidas por tenant, organizadas por categoria'; -- Indices CREATE INDEX IF NOT EXISTS idx_tenant_settings_tenant ON tenants.tenant_settings(tenant_id); CREATE INDEX IF NOT EXISTS idx_tenant_settings_category ON tenants.tenant_settings(tenant_id, category); -- Trigger para updated_at DROP TRIGGER IF EXISTS tenant_settings_updated_at ON tenants.tenant_settings; CREATE TRIGGER tenant_settings_updated_at BEFORE UPDATE ON tenants.tenant_settings FOR EACH ROW EXECUTE FUNCTION tenants.update_tenant_timestamp(); -- RLS Policy (tenant_settings aislado por tenant) ALTER TABLE tenants.tenant_settings ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_settings_isolation ON tenants.tenant_settings FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT, UPDATE ON tenants.tenants TO trading_app; GRANT SELECT, INSERT, UPDATE, DELETE ON tenants.tenant_settings TO trading_app; GRANT SELECT ON tenants.tenants TO trading_readonly; GRANT SELECT ON tenants.tenant_settings TO trading_readonly;