-- ============================================================================ -- CONSTRUCTION Schema DDL - Gestión de Obras (COMPLETO) -- Modulos: MAI-002, MAI-003, MAI-005, MAI-009, MAI-012 -- Version: 2.0.0 -- Fecha: 2025-12-08 -- ============================================================================ -- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) -- Este archivo es parte de la fuente de verdad DDL. -- ============================================================================ -- Verificar que ERP-Core está instalado DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'tenants') THEN RAISE EXCEPTION 'Tabla auth.tenants no existe. ERP-Core debe estar instalado'; END IF; IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN RAISE EXCEPTION 'Tabla auth.users no existe. ERP-Core debe estar instalado'; END IF; END $$; -- Crear schema si no existe CREATE SCHEMA IF NOT EXISTS construction; -- ============================================================================ -- TYPES (ENUMs) -- ============================================================================ DO $$ BEGIN CREATE TYPE construction.project_status AS ENUM ( 'draft', 'planning', 'in_progress', 'paused', 'completed', 'cancelled' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.lot_status AS ENUM ( 'available', 'reserved', 'sold', 'under_construction', 'delivered', 'warranty' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.prototype_type AS ENUM ( 'horizontal', 'vertical', 'commercial', 'mixed' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.advance_status AS ENUM ( 'pending', 'captured', 'reviewed', 'approved', 'rejected' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.quality_status AS ENUM ( 'pending', 'in_review', 'approved', 'rejected', 'rework' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.contract_type AS ENUM ( 'fixed_price', 'unit_price', 'cost_plus', 'mixed' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE construction.contract_status AS ENUM ( 'draft', 'pending_approval', 'active', 'suspended', 'terminated', 'closed' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ============================================================================ -- TABLES - ESTRUCTURA DE PROYECTO -- ============================================================================ -- Tabla: fraccionamientos (desarrollo inmobiliario) CREATE TABLE IF NOT EXISTS construction.fraccionamientos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, address TEXT, city VARCHAR(100), state VARCHAR(100), zip_code VARCHAR(10), location GEOMETRY(POINT, 4326), total_area_m2 DECIMAL(12,2), buildable_area_m2 DECIMAL(12,2), total_lots INTEGER DEFAULT 0, status construction.project_status NOT NULL DEFAULT 'draft', start_date DATE, expected_end_date DATE, actual_end_date DATE, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_fraccionamientos_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: etapas (fases del fraccionamiento) CREATE TABLE IF NOT EXISTS construction.etapas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, sequence INTEGER NOT NULL DEFAULT 1, total_lots INTEGER DEFAULT 0, status construction.project_status NOT NULL DEFAULT 'draft', start_date DATE, expected_end_date DATE, actual_end_date DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_etapas_code_fracc UNIQUE (fraccionamiento_id, code) ); -- Tabla: manzanas (agrupación de lotes) CREATE TABLE IF NOT EXISTS construction.manzanas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, etapa_id UUID NOT NULL REFERENCES construction.etapas(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(100), total_lots INTEGER DEFAULT 0, polygon GEOMETRY(POLYGON, 4326), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_manzanas_code_etapa UNIQUE (etapa_id, code) ); -- Tabla: prototipos (tipos de vivienda) - definida antes de lotes CREATE TABLE IF NOT EXISTS construction.prototipos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, type construction.prototype_type NOT NULL DEFAULT 'horizontal', area_construction_m2 DECIMAL(10,2), area_terrain_m2 DECIMAL(10,2), bedrooms INTEGER DEFAULT 0, bathrooms DECIMAL(3,1) DEFAULT 0, parking_spaces INTEGER DEFAULT 0, floors INTEGER DEFAULT 1, base_price DECIMAL(14,2), blueprint_url VARCHAR(500), render_url VARCHAR(500), is_active BOOLEAN NOT NULL DEFAULT TRUE, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_prototipos_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: lotes (unidades vendibles horizontal) CREATE TABLE IF NOT EXISTS construction.lotes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, manzana_id UUID NOT NULL REFERENCES construction.manzanas(id) ON DELETE CASCADE, prototipo_id UUID REFERENCES construction.prototipos(id), code VARCHAR(30) NOT NULL, official_number VARCHAR(50), area_m2 DECIMAL(10,2), front_m DECIMAL(8,2), depth_m DECIMAL(8,2), status construction.lot_status NOT NULL DEFAULT 'available', location GEOMETRY(POINT, 4326), polygon GEOMETRY(POLYGON, 4326), price_base DECIMAL(14,2), price_final DECIMAL(14,2), buyer_id UUID, sale_date DATE, delivery_date DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_lotes_code_manzana UNIQUE (manzana_id, code) ); -- ============================================================================ -- TABLES - ESTRUCTURA VERTICAL (TORRES) -- ============================================================================ -- Tabla: torres (edificios verticales) CREATE TABLE IF NOT EXISTS construction.torres ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, etapa_id UUID NOT NULL REFERENCES construction.etapas(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, total_floors INTEGER NOT NULL DEFAULT 1, total_units INTEGER DEFAULT 0, status construction.project_status NOT NULL DEFAULT 'draft', location GEOMETRY(POINT, 4326), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_torres_code_etapa UNIQUE (etapa_id, code) ); -- Tabla: niveles (pisos de torre) CREATE TABLE IF NOT EXISTS construction.niveles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, torre_id UUID NOT NULL REFERENCES construction.torres(id) ON DELETE CASCADE, floor_number INTEGER NOT NULL, name VARCHAR(50), total_units INTEGER DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_niveles_floor_torre UNIQUE (torre_id, floor_number) ); -- Tabla: departamentos (unidades en torre) CREATE TABLE IF NOT EXISTS construction.departamentos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, nivel_id UUID NOT NULL REFERENCES construction.niveles(id) ON DELETE CASCADE, prototipo_id UUID REFERENCES construction.prototipos(id), code VARCHAR(30) NOT NULL, unit_number VARCHAR(20) NOT NULL, area_m2 DECIMAL(10,2), status construction.lot_status NOT NULL DEFAULT 'available', price_base DECIMAL(14,2), price_final DECIMAL(14,2), buyer_id UUID, sale_date DATE, delivery_date DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_departamentos_code_nivel UNIQUE (nivel_id, code) ); -- ============================================================================ -- TABLES - CONCEPTOS Y PRESUPUESTOS -- ============================================================================ -- Tabla: conceptos (catálogo de conceptos de obra) CREATE TABLE IF NOT EXISTS construction.conceptos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, parent_id UUID REFERENCES construction.conceptos(id), code VARCHAR(50) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, unit_id UUID, unit_price DECIMAL(12,4), is_composite BOOLEAN NOT NULL DEFAULT FALSE, level INTEGER NOT NULL DEFAULT 0, path VARCHAR(500), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_conceptos_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: presupuestos (presupuesto por prototipo/obra) CREATE TABLE IF NOT EXISTS construction.presupuestos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), prototipo_id UUID REFERENCES construction.prototipos(id), code VARCHAR(30) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, version INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT TRUE, total_amount DECIMAL(16,2) DEFAULT 0, currency_id UUID, approved_at TIMESTAMPTZ, approved_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_presupuestos_code_version UNIQUE (tenant_id, code, version) ); -- Tabla: presupuesto_partidas (líneas del presupuesto) CREATE TABLE IF NOT EXISTS construction.presupuesto_partidas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, presupuesto_id UUID NOT NULL REFERENCES construction.presupuestos(id) ON DELETE CASCADE, concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), sequence INTEGER NOT NULL DEFAULT 0, quantity DECIMAL(12,4) NOT NULL DEFAULT 0, unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, total_amount DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_partidas_presupuesto_concepto UNIQUE (presupuesto_id, concepto_id) ); -- ============================================================================ -- TABLES - AVANCES Y CONTROL DE OBRA -- ============================================================================ -- Tabla: programa_obra (programa maestro) CREATE TABLE IF NOT EXISTS construction.programa_obra ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), code VARCHAR(30) NOT NULL, name VARCHAR(255) NOT NULL, version INTEGER NOT NULL DEFAULT 1, start_date DATE NOT NULL, end_date DATE NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_programa_code_version UNIQUE (tenant_id, code, version) ); -- Tabla: programa_actividades (actividades del programa) CREATE TABLE IF NOT EXISTS construction.programa_actividades ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, programa_id UUID NOT NULL REFERENCES construction.programa_obra(id) ON DELETE CASCADE, concepto_id UUID REFERENCES construction.conceptos(id), parent_id UUID REFERENCES construction.programa_actividades(id), name VARCHAR(255) NOT NULL, sequence INTEGER NOT NULL DEFAULT 0, planned_start DATE, planned_end DATE, planned_quantity DECIMAL(12,4) DEFAULT 0, planned_weight DECIMAL(8,4) DEFAULT 0, wbs_code VARCHAR(50), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ); -- Tabla: avances_obra (captura de avances) CREATE TABLE IF NOT EXISTS construction.avances_obra ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, lote_id UUID REFERENCES construction.lotes(id), departamento_id UUID REFERENCES construction.departamentos(id), concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), capture_date DATE NOT NULL, quantity_executed DECIMAL(12,4) NOT NULL DEFAULT 0, percentage_executed DECIMAL(5,2) DEFAULT 0, status construction.advance_status NOT NULL DEFAULT 'pending', notes TEXT, captured_by UUID NOT NULL REFERENCES auth.users(id), reviewed_by UUID REFERENCES auth.users(id), reviewed_at TIMESTAMPTZ, approved_by UUID REFERENCES auth.users(id), approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT chk_avances_lote_or_depto CHECK ( (lote_id IS NOT NULL AND departamento_id IS NULL) OR (lote_id IS NULL AND departamento_id IS NOT NULL) ) ); -- Tabla: fotos_avance (evidencia fotográfica) CREATE TABLE IF NOT EXISTS construction.fotos_avance ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, avance_id UUID NOT NULL REFERENCES construction.avances_obra(id) ON DELETE CASCADE, file_url VARCHAR(500) NOT NULL, file_name VARCHAR(255), file_size INTEGER, mime_type VARCHAR(50), description TEXT, location GEOMETRY(POINT, 4326), captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ); -- Tabla: bitacora_obra (registro de bitácora) CREATE TABLE IF NOT EXISTS construction.bitacora_obra ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), entry_date DATE NOT NULL, entry_number INTEGER NOT NULL, weather VARCHAR(50), temperature_max DECIMAL(4,1), temperature_min DECIMAL(4,1), workers_count INTEGER DEFAULT 0, description TEXT NOT NULL, observations TEXT, incidents TEXT, registered_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_bitacora_fracc_number UNIQUE (fraccionamiento_id, entry_number) ); -- ============================================================================ -- TABLES - CALIDAD Y POSTVENTA (MAI-009) -- ============================================================================ -- Tabla: checklists (plantillas de verificación) CREATE TABLE IF NOT EXISTS construction.checklists ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, code VARCHAR(30) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, prototipo_id UUID REFERENCES construction.prototipos(id), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_checklists_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: checklist_items (items del checklist) CREATE TABLE IF NOT EXISTS construction.checklist_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, checklist_id UUID NOT NULL REFERENCES construction.checklists(id) ON DELETE CASCADE, sequence INTEGER NOT NULL DEFAULT 0, name VARCHAR(255) NOT NULL, description TEXT, is_required BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ); -- Tabla: inspecciones (inspecciones de calidad) CREATE TABLE IF NOT EXISTS construction.inspecciones ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, checklist_id UUID NOT NULL REFERENCES construction.checklists(id), lote_id UUID REFERENCES construction.lotes(id), departamento_id UUID REFERENCES construction.departamentos(id), inspection_date DATE NOT NULL, status construction.quality_status NOT NULL DEFAULT 'pending', inspector_id UUID NOT NULL REFERENCES auth.users(id), notes TEXT, approved_by UUID REFERENCES auth.users(id), approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ); -- Tabla: inspeccion_resultados (resultados por item) CREATE TABLE IF NOT EXISTS construction.inspeccion_resultados ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, inspeccion_id UUID NOT NULL REFERENCES construction.inspecciones(id) ON DELETE CASCADE, checklist_item_id UUID NOT NULL REFERENCES construction.checklist_items(id), is_passed BOOLEAN, notes TEXT, photo_url VARCHAR(500), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id) ); -- Tabla: tickets_postventa (tickets de garantía) CREATE TABLE IF NOT EXISTS construction.tickets_postventa ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, lote_id UUID REFERENCES construction.lotes(id), departamento_id UUID REFERENCES construction.departamentos(id), ticket_number VARCHAR(30) NOT NULL, reported_date DATE NOT NULL, category VARCHAR(50), description TEXT NOT NULL, priority VARCHAR(20) DEFAULT 'medium', status VARCHAR(20) NOT NULL DEFAULT 'open', assigned_to UUID REFERENCES auth.users(id), resolution TEXT, resolved_at TIMESTAMPTZ, resolved_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_tickets_number_tenant UNIQUE (tenant_id, ticket_number) ); -- ============================================================================ -- TABLES - CONTRATOS Y SUBCONTRATOS (MAI-012) -- ============================================================================ -- Tabla: subcontratistas CREATE TABLE IF NOT EXISTS construction.subcontratistas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, partner_id UUID, code VARCHAR(20) NOT NULL, name VARCHAR(255) NOT NULL, legal_name VARCHAR(255), tax_id VARCHAR(20), specialty VARCHAR(100), contact_name VARCHAR(100), contact_phone VARCHAR(20), contact_email VARCHAR(100), address TEXT, rating DECIMAL(3,2), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_subcontratistas_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: contratos (contratos con subcontratistas) CREATE TABLE IF NOT EXISTS construction.contratos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, subcontratista_id UUID NOT NULL REFERENCES construction.subcontratistas(id), fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), contract_number VARCHAR(30) NOT NULL, contract_type construction.contract_type NOT NULL DEFAULT 'unit_price', name VARCHAR(255) NOT NULL, description TEXT, start_date DATE NOT NULL, end_date DATE, total_amount DECIMAL(16,2), advance_percentage DECIMAL(5,2) DEFAULT 0, retention_percentage DECIMAL(5,2) DEFAULT 5, status construction.contract_status NOT NULL DEFAULT 'draft', signed_at TIMESTAMPTZ, signed_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), CONSTRAINT uq_contratos_number_tenant UNIQUE (tenant_id, contract_number) ); -- Tabla: contrato_partidas (líneas del contrato) CREATE TABLE IF NOT EXISTS construction.contrato_partidas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, contrato_id UUID NOT NULL REFERENCES construction.contratos(id) ON DELETE CASCADE, concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), quantity DECIMAL(12,4) NOT NULL DEFAULT 0, unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, total_amount DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ, updated_by UUID REFERENCES auth.users(id), deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ); -- ============================================================================ -- INDICES -- ============================================================================ -- Fraccionamientos CREATE INDEX IF NOT EXISTS idx_fraccionamientos_tenant_id ON construction.fraccionamientos(tenant_id); CREATE INDEX IF NOT EXISTS idx_fraccionamientos_status ON construction.fraccionamientos(status); CREATE INDEX IF NOT EXISTS idx_fraccionamientos_code ON construction.fraccionamientos(code); -- Etapas CREATE INDEX IF NOT EXISTS idx_etapas_tenant_id ON construction.etapas(tenant_id); CREATE INDEX IF NOT EXISTS idx_etapas_fraccionamiento_id ON construction.etapas(fraccionamiento_id); -- Manzanas CREATE INDEX IF NOT EXISTS idx_manzanas_tenant_id ON construction.manzanas(tenant_id); CREATE INDEX IF NOT EXISTS idx_manzanas_etapa_id ON construction.manzanas(etapa_id); -- Lotes CREATE INDEX IF NOT EXISTS idx_lotes_tenant_id ON construction.lotes(tenant_id); CREATE INDEX IF NOT EXISTS idx_lotes_manzana_id ON construction.lotes(manzana_id); CREATE INDEX IF NOT EXISTS idx_lotes_prototipo_id ON construction.lotes(prototipo_id); CREATE INDEX IF NOT EXISTS idx_lotes_status ON construction.lotes(status); -- Torres CREATE INDEX IF NOT EXISTS idx_torres_tenant_id ON construction.torres(tenant_id); CREATE INDEX IF NOT EXISTS idx_torres_etapa_id ON construction.torres(etapa_id); -- Niveles CREATE INDEX IF NOT EXISTS idx_niveles_tenant_id ON construction.niveles(tenant_id); CREATE INDEX IF NOT EXISTS idx_niveles_torre_id ON construction.niveles(torre_id); -- Departamentos CREATE INDEX IF NOT EXISTS idx_departamentos_tenant_id ON construction.departamentos(tenant_id); CREATE INDEX IF NOT EXISTS idx_departamentos_nivel_id ON construction.departamentos(nivel_id); CREATE INDEX IF NOT EXISTS idx_departamentos_status ON construction.departamentos(status); -- Prototipos CREATE INDEX IF NOT EXISTS idx_prototipos_tenant_id ON construction.prototipos(tenant_id); CREATE INDEX IF NOT EXISTS idx_prototipos_type ON construction.prototipos(type); -- Conceptos CREATE INDEX IF NOT EXISTS idx_conceptos_tenant_id ON construction.conceptos(tenant_id); CREATE INDEX IF NOT EXISTS idx_conceptos_parent_id ON construction.conceptos(parent_id); CREATE INDEX IF NOT EXISTS idx_conceptos_code ON construction.conceptos(code); -- Presupuestos CREATE INDEX IF NOT EXISTS idx_presupuestos_tenant_id ON construction.presupuestos(tenant_id); CREATE INDEX IF NOT EXISTS idx_presupuestos_fraccionamiento_id ON construction.presupuestos(fraccionamiento_id); -- Avances CREATE INDEX IF NOT EXISTS idx_avances_tenant_id ON construction.avances_obra(tenant_id); CREATE INDEX IF NOT EXISTS idx_avances_lote_id ON construction.avances_obra(lote_id); CREATE INDEX IF NOT EXISTS idx_avances_concepto_id ON construction.avances_obra(concepto_id); CREATE INDEX IF NOT EXISTS idx_avances_capture_date ON construction.avances_obra(capture_date); -- Bitacora CREATE INDEX IF NOT EXISTS idx_bitacora_tenant_id ON construction.bitacora_obra(tenant_id); CREATE INDEX IF NOT EXISTS idx_bitacora_fraccionamiento_id ON construction.bitacora_obra(fraccionamiento_id); -- Inspecciones CREATE INDEX IF NOT EXISTS idx_inspecciones_tenant_id ON construction.inspecciones(tenant_id); CREATE INDEX IF NOT EXISTS idx_inspecciones_status ON construction.inspecciones(status); -- Tickets CREATE INDEX IF NOT EXISTS idx_tickets_tenant_id ON construction.tickets_postventa(tenant_id); CREATE INDEX IF NOT EXISTS idx_tickets_status ON construction.tickets_postventa(status); -- Subcontratistas CREATE INDEX IF NOT EXISTS idx_subcontratistas_tenant_id ON construction.subcontratistas(tenant_id); -- Contratos CREATE INDEX IF NOT EXISTS idx_contratos_tenant_id ON construction.contratos(tenant_id); CREATE INDEX IF NOT EXISTS idx_contratos_subcontratista_id ON construction.contratos(subcontratista_id); CREATE INDEX IF NOT EXISTS idx_contratos_fraccionamiento_id ON construction.contratos(fraccionamiento_id); -- ============================================================================ -- ROW LEVEL SECURITY (RLS) -- ============================================================================ ALTER TABLE construction.fraccionamientos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.etapas ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.manzanas ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.lotes ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.torres ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.niveles ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.departamentos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.prototipos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.conceptos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.presupuestos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.presupuesto_partidas ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.programa_obra ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.programa_actividades ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.avances_obra ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.fotos_avance ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.bitacora_obra ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.checklists ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.checklist_items ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.inspecciones ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.inspeccion_resultados ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.tickets_postventa ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.subcontratistas ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.contratos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.contrato_partidas ENABLE ROW LEVEL SECURITY; -- Policies de tenant isolation usando current_setting DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_fraccionamientos ON construction.fraccionamientos; CREATE POLICY tenant_isolation_fraccionamientos ON construction.fraccionamientos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_etapas ON construction.etapas; CREATE POLICY tenant_isolation_etapas ON construction.etapas FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_manzanas ON construction.manzanas; CREATE POLICY tenant_isolation_manzanas ON construction.manzanas FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_lotes ON construction.lotes; CREATE POLICY tenant_isolation_lotes ON construction.lotes FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_torres ON construction.torres; CREATE POLICY tenant_isolation_torres ON construction.torres FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_niveles ON construction.niveles; CREATE POLICY tenant_isolation_niveles ON construction.niveles FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_departamentos ON construction.departamentos; CREATE POLICY tenant_isolation_departamentos ON construction.departamentos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_prototipos ON construction.prototipos; CREATE POLICY tenant_isolation_prototipos ON construction.prototipos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_conceptos ON construction.conceptos; CREATE POLICY tenant_isolation_conceptos ON construction.conceptos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_presupuestos ON construction.presupuestos; CREATE POLICY tenant_isolation_presupuestos ON construction.presupuestos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_presupuesto_partidas ON construction.presupuesto_partidas; CREATE POLICY tenant_isolation_presupuesto_partidas ON construction.presupuesto_partidas FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_programa_obra ON construction.programa_obra; CREATE POLICY tenant_isolation_programa_obra ON construction.programa_obra FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_programa_actividades ON construction.programa_actividades; CREATE POLICY tenant_isolation_programa_actividades ON construction.programa_actividades FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_avances_obra ON construction.avances_obra; CREATE POLICY tenant_isolation_avances_obra ON construction.avances_obra FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_fotos_avance ON construction.fotos_avance; CREATE POLICY tenant_isolation_fotos_avance ON construction.fotos_avance FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_bitacora_obra ON construction.bitacora_obra; CREATE POLICY tenant_isolation_bitacora_obra ON construction.bitacora_obra FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_checklists ON construction.checklists; CREATE POLICY tenant_isolation_checklists ON construction.checklists FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_checklist_items ON construction.checklist_items; CREATE POLICY tenant_isolation_checklist_items ON construction.checklist_items FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_inspecciones ON construction.inspecciones; CREATE POLICY tenant_isolation_inspecciones ON construction.inspecciones FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_inspeccion_resultados ON construction.inspeccion_resultados; CREATE POLICY tenant_isolation_inspeccion_resultados ON construction.inspeccion_resultados FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_tickets_postventa ON construction.tickets_postventa; CREATE POLICY tenant_isolation_tickets_postventa ON construction.tickets_postventa FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_subcontratistas ON construction.subcontratistas; CREATE POLICY tenant_isolation_subcontratistas ON construction.subcontratistas FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_contratos ON construction.contratos; CREATE POLICY tenant_isolation_contratos ON construction.contratos FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; DO $$ BEGIN DROP POLICY IF EXISTS tenant_isolation_contrato_partidas ON construction.contrato_partidas; CREATE POLICY tenant_isolation_contrato_partidas ON construction.contrato_partidas FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); EXCEPTION WHEN undefined_object THEN NULL; END $$; -- ============================================================================ -- COMENTARIOS -- ============================================================================ COMMENT ON SCHEMA construction IS 'Schema de construcción: obras, lotes, avances, calidad, contratos'; COMMENT ON TABLE construction.fraccionamientos IS 'Desarrollos inmobiliarios/fraccionamientos'; COMMENT ON TABLE construction.etapas IS 'Etapas/fases de un fraccionamiento'; COMMENT ON TABLE construction.manzanas IS 'Manzanas dentro de una etapa'; COMMENT ON TABLE construction.lotes IS 'Lotes/terrenos vendibles (horizontal)'; COMMENT ON TABLE construction.torres IS 'Torres/edificios (vertical)'; COMMENT ON TABLE construction.niveles IS 'Pisos de una torre'; COMMENT ON TABLE construction.departamentos IS 'Departamentos/unidades en torre'; COMMENT ON TABLE construction.prototipos IS 'Tipos de vivienda/prototipos'; COMMENT ON TABLE construction.conceptos IS 'Catálogo de conceptos de obra'; COMMENT ON TABLE construction.presupuestos IS 'Presupuestos por prototipo u obra'; COMMENT ON TABLE construction.avances_obra IS 'Captura de avances físicos'; COMMENT ON TABLE construction.bitacora_obra IS 'Bitácora diaria de obra'; COMMENT ON TABLE construction.checklists IS 'Plantillas de verificación'; COMMENT ON TABLE construction.inspecciones IS 'Inspecciones de calidad'; COMMENT ON TABLE construction.tickets_postventa IS 'Tickets de garantía'; COMMENT ON TABLE construction.subcontratistas IS 'Catálogo de subcontratistas'; COMMENT ON TABLE construction.contratos IS 'Contratos con subcontratistas'; -- ============================================================================ -- FIN DEL SCHEMA CONSTRUCTION -- Total tablas: 24 -- ============================================================================