diff --git a/HERENCIA-ERP-CORE.md b/HERENCIA-ERP-CORE.md new file mode 100644 index 0000000..72e7a69 --- /dev/null +++ b/HERENCIA-ERP-CORE.md @@ -0,0 +1,362 @@ +# Referencia de Base de Datos - ERP Construcción + +**Fecha:** 2025-12-09 +**Versión:** 1.2 +**Proyecto:** ERP Construcción +**Nivel:** 2B.2 (Proyecto Independiente) + +--- + +## RESUMEN + +ERP Construcción es un **proyecto independiente** que implementa y adapta patrones del ERP-Core para el dominio de construcción de vivienda. No es una extensión del core, sino un sistema autónomo que: + +1. **Implementa** schemas propios basados en patrones del core +2. **Adapta** estructuras de datos al dominio de construcción +3. **Reutiliza** código y patrones donde tiene sentido +4. **Opera independientemente** del ERP-Core + +**DDL de Referencia (Core):** `apps/erp-core/database/ddl/` +**DDL Propio:** `database/schemas/` + +--- + +## ARQUITECTURA DEL PROYECTO + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ERP CORE (Referencia) │ +│ Patrones, specs y estructuras reutilizables │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ auth │ │ core │ │inventory│ │ financial│ │ +│ │ patrones│ │ patrones│ │ patrones│ │ patrones │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ REFERENCIA / FORK + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ERP CONSTRUCCIÓN (Proyecto Independiente) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │construction │ │ hr │ │ hse │ │ +│ │ 24 tbl │ │ 8 tbl │ │ 58 tbl │ │ +│ │ (proyectos) │ │ (empleados) │ │ (seguridad) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ estimates │ │ infonavit │ │ inventory │ │ +│ │ 8 tbl │ │ 8 tbl │ │ 4 tbl │ │ +│ │(estimación) │ │ (ruv) │ │ (ext) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ │ +│ │ purchase │ Schemas propios: 7 │ +│ │ 5 tbl │ Tablas propias: 110 │ +│ │ (ext) │ Opera de forma INDEPENDIENTE │ +│ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## PATRONES REUTILIZADOS DEL CORE + +Los siguientes patrones del ERP-Core fueron **adaptados e implementados** en este proyecto: + +| Patrón del Core | Adaptación en Construcción | +|-----------------|---------------------------| +| `auth.*` | Implementación propia de autenticación multi-tenant | +| `core.partners` | Contratistas, proveedores, clientes | +| `inventory.*` | Materiales de construcción, almacenes de obra | +| `projects.*` | Obras, fraccionamientos, etapas | +| `hr.*` | Personal de obra, cuadrillas, asistencias | + +**Nota:** Este proyecto NO depende del ERP-Core para ejecutarse. Implementa sus propios schemas basados en los patrones de referencia. + +--- + +## SCHEMAS ESPECÍFICOS DE CONSTRUCCIÓN + +### 1. Schema `construction` (24 tablas) + +**Propósito:** Gestión de proyectos de obra, estructura y avances + +```sql +-- DDL: 01-construction-schema-ddl.sql +-- Estructura de proyecto (8 tablas): +-- fraccionamientos, etapas, manzanas, lotes, torres, niveles, departamentos, prototipos +-- Presupuestos y Conceptos (3 tablas): +-- conceptos, presupuestos, presupuesto_partidas +-- Programación y Avances (5 tablas): +-- programa_obra, programa_actividades, avances_obra, fotos_avance, bitacora_obra +-- Calidad (5 tablas): +-- checklists, checklist_items, inspecciones, inspeccion_resultados, tickets_postventa +-- Contratos (3 tablas): +-- subcontratistas, contratos, contrato_partidas +``` + +### 2. Schema `hr` extendido (8 tablas) + +**Propósito:** Gestión de personal de obra, asistencias, destajo + +```sql +-- DDL: 02-hr-schema-ddl.sql +-- Extiende: hr schema del core + +hr.employee_construction -- Extensión empleados construcción +hr.asistencias -- Registro con GPS/biométrico +hr.asistencia_biometrico -- Datos biométricos +hr.geocercas -- Validación GPS (PostGIS) +hr.destajo -- Trabajo a destajo +hr.destajo_detalle -- Mediciones destajo +hr.cuadrillas -- Equipos de trabajo +hr.cuadrilla_miembros -- Miembros cuadrillas +``` + +### 3. Schema `hse` (58 tablas) + +**Propósito:** Health, Safety & Environment + +```sql +-- DDL: 03-hse-schema-ddl.sql +-- Implementa 8 requerimientos funcionales (RF-MAA017-001 a 008) + +Grupos de tablas: +- Gestión de Incidentes (5 tablas) +- Control de Capacitaciones (6 tablas) +- Inspecciones de Seguridad (7 tablas) +- Control de EPP (7 tablas) +- Cumplimiento STPS (11 tablas) +- Gestión Ambiental (9 tablas) +- Permisos de Trabajo (8 tablas) +- Indicadores HSE (7 tablas) +``` + +### 4. Schema `estimates` (8 tablas) + +**Propósito:** Estimaciones, anticipos, retenciones + +```sql +-- DDL: 04-estimates-schema-ddl.sql +-- Módulo: MAI-008 (Estimaciones y Facturación) + +estimates.estimaciones -- Estimaciones de obra +estimates.estimacion_conceptos -- Conceptos estimados +estimates.generadores -- Números generadores +estimates.anticipos -- Anticipos de obra +estimates.amortizaciones -- Amortización de anticipos +estimates.retenciones -- Retenciones (garantía, IMSS, ISR) +estimates.fondo_garantia -- Fondo de garantía +estimates.estimacion_workflow -- Workflow de aprobación +``` + +### 5. Schema `infonavit` (8 tablas) + +**Propósito:** Integración INFONAVIT, RUV, derechohabientes + +```sql +-- DDL: 05-infonavit-schema-ddl.sql +-- Módulos: MAI-010/011 (CRM Derechohabientes, Integración INFONAVIT) + +infonavit.registro_infonavit -- Registro RUV +infonavit.oferta_vivienda -- Oferta registrada +infonavit.derechohabientes -- Derechohabientes +infonavit.asignacion_vivienda -- Asignaciones +infonavit.actas -- Actas de entrega +infonavit.acta_viviendas -- Viviendas en acta +infonavit.reportes_infonavit -- Reportes RUV +infonavit.historico_puntos -- Histórico puntos ecológicos +``` + +### 6. Schema `inventory` extensión (4 tablas) + +**Propósito:** Almacenes de proyecto, requisiciones de obra + +```sql +-- DDL: 06-inventory-ext-schema-ddl.sql +-- Extiende: inventory schema del core + +inventory.almacenes_proyecto -- Almacenes por obra +inventory.requisiciones_obra -- Requisiciones desde obra +inventory.requisicion_lineas -- Líneas de requisición +inventory.consumos_obra -- Consumos por lote/concepto +``` + +### 7. Schema `purchase` extensión (5 tablas) + +**Propósito:** Órdenes de compra construcción, comparativos + +```sql +-- DDL: 07-purchase-ext-schema-ddl.sql +-- Extiende: purchase schema del core + +purchase.purchase_order_construction -- Extensión OC +purchase.supplier_construction -- Extensión proveedores +purchase.comparativo_cotizaciones -- Cuadro comparativo +purchase.comparativo_proveedores -- Proveedores en comparativo +purchase.comparativo_productos -- Productos cotizados +``` + +--- + +## ORDEN DE EJECUCIÓN DDL + +Para recrear la base de datos completa: + +```bash +# PASO 1: Cargar ERP Core (base) +cd apps/erp-core/database +./scripts/reset-database.sh --force + +# PASO 2: Cargar extensiones de Construcción (orden importante) +cd apps/verticales/construccion/database +psql $DATABASE_URL -f schemas/01-construction-schema-ddl.sql # 24 tablas +psql $DATABASE_URL -f schemas/02-hr-schema-ddl.sql # 8 tablas +psql $DATABASE_URL -f schemas/03-hse-schema-ddl.sql # 58 tablas +psql $DATABASE_URL -f schemas/04-estimates-schema-ddl.sql # 8 tablas +psql $DATABASE_URL -f schemas/05-infonavit-schema-ddl.sql # 8 tablas +psql $DATABASE_URL -f schemas/06-inventory-ext-schema-ddl.sql # 4 tablas +psql $DATABASE_URL -f schemas/07-purchase-ext-schema-ddl.sql # 5 tablas +``` + +**Nota:** Los archivos 06 y 07 dependen de que 01-construction esté instalado. + +--- + +## DEPENDENCIAS CRUZADAS + +### Tablas de Construcción que dependen del Core + +| Tabla Construcción | Depende de (Core) | +|--------------------|-------------------| +| `construccion.proyectos` | `core.partners` (cliente) | +| `construccion.proyectos` | `auth.users` (created_by) | +| `construccion.fraccionamientos` | `construccion.proyectos` | +| `hr.employees` | `auth.users` | +| `hr.employee_fraccionamientos` | `construccion.fraccionamientos` | +| `hse.incidentes` | `construccion.fraccionamientos` | +| `hse.incidente_involucrados` | `hr.employees` | +| `hse.*` | `auth.users` (auditoria) | + +--- + +## SPECS DEL CORE IMPLEMENTADAS + +| Spec Core | Aplicación en Construcción | Estado | +|-----------|---------------------------|--------| +| SPEC-VALORACION-INVENTARIO | Materiales de construcción | ✅ DDL LISTO | +| SPEC-TRAZABILIDAD-LOTES-SERIES | Lotes de concreto, acero | ✅ DDL LISTO | +| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | Partidas de obra | PENDIENTE | +| SPEC-MAIL-THREAD-TRACKING | Historial de presupuestos | PENDIENTE | +| SPEC-WIZARD-TRANSIENT-MODEL | Asistentes de estimaciones | PENDIENTE | + +### Correcciones de DDL Core (2025-12-08) + +El DDL del ERP-Core fue corregido para resolver FK inválidas: + +1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`) +2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`) +3. **category_stock_accounts**: FK corregida a `core.product_categories` +4. **product_categories**: ALTERs ahora apuntan a schema `core` + +Estas correcciones permiten que el DDL de inventory se ejecute correctamente. + +### Correcciones de DDL Construcción (2025-12-08) + +El DDL de la vertical Construcción fue corregido para alinearse con ERP-Core: + +| Archivo | Correcciones | Detalle | +|---------|--------------|---------| +| `01-construction-schema-ddl.sql` | 4 FK | `core.tenants` → `auth.tenants`, `core.users` → `auth.users` | +| `02-hr-schema-ddl.sql` | 4 FK | Referencias corregidas a `auth.*` | +| `03-hse-schema-ddl.sql` | 42 FK | Todas las referencias corregidas | +| **Total** | **50 FK** | Ahora usa `auth.tenants` y `auth.users` correctamente | + +**Verificaciones de prerequisitos actualizadas:** +- Los DDL ahora validan que `auth.tenants` y `auth.users` existan antes de crear tablas +- ERP-Core debe estar instalado antes de ejecutar DDL de Construcción + +--- + +## MAPEO DE NOMENCLATURA + +| Core | Construcción | +|------|--------------| +| `core.partners` | Contratistas, proveedores | +| `inventory.products` | Materiales de construcción | +| `inventory.locations` | Almacenes de obra | +| `projects.projects` | Base para `construccion.proyectos` | +| `hr.employees` | Personal de obra | +| `purchase.orders` | Órdenes de compra de materiales | + +--- + +## VALIDACIÓN DE HERENCIA + +### Verificar schemas heredados + +```sql +-- Verificar que existen los schemas del core +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name IN ('auth', 'core', 'financial', 'inventory', + 'purchase', 'projects', 'hr', 'analytics', 'system'); +``` + +### Verificar extensiones de construcción + +```sql +-- Verificar schemas específicos +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name IN ('construccion', 'hse'); + +-- Contar tablas por schema +SELECT schemaname, COUNT(*) as tables +FROM pg_tables +WHERE schemaname IN ('construccion', 'hr', 'hse') +GROUP BY schemaname; +``` + +--- + +## SPECS DEL CORE APLICABLES + +Según el [MAPEO-SPECS-VERTICALES.md](../../../../erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md): + +| Categoría | Total | Obligatorias | Opcionales | No Aplican | +|-----------|-------|--------------|------------|------------| +| **Construcción** | 30 | 22 | 4 | 4 | + +### SPECS Críticas para Construcción + +| SPEC | Aplicación | Estado DDL | +|------|------------|------------| +| SPEC-VALORACION-INVENTARIO | Costeo de materiales | ✅ DDL LISTO | +| SPEC-TRAZABILIDAD-LOTES-SERIES | Lotes de concreto, acero | ✅ DDL LISTO | +| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | Partidas de obra | PENDIENTE | +| SPEC-PRESUPUESTOS-REVISIONES | Control presupuestal | PENDIENTE | +| SPEC-RRHH-EVALUACIONES-SKILLS | Personal de obra | PENDIENTE | + +### SPECS No Aplicables + +- `SPEC-INTEGRACION-CALENDAR` - Sin necesidad de calendario externo +- `SPEC-OAUTH2-SOCIAL-LOGIN` - Opcional, no crítico +- `SPEC-INVENTARIOS-CICLICOS` - Opcional para construcción +- `SPEC-CONSOLIDACION-FINANCIERA` - Opcional para construcción + +--- + +## REFERENCIAS + +- ERP Core DDL: `apps/erp-core/database/ddl/` +- ERP Core README: `apps/erp-core/database/README.md` +- MAPEO-SPECS-VERTICALES: `apps/erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md` +- DATABASE_INVENTORY.yml: `orchestration/inventarios/` + +--- + +**Documento de herencia oficial** +**Última actualización:** 2025-12-09 +**Total schemas:** 7 | **Total tablas:** 110 diff --git a/README.md b/README.md index 1dee2a8..c63e6e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,185 @@ -# erp-construccion-database-v2 +# Database - MVP Sistema Administración de Obra -Database de erp-construccion - Workspace V2 \ No newline at end of file +**Stack:** PostgreSQL 15+ con PostGIS +**Versión:** 1.0.0 +**Fecha:** 2025-11-20 + +--- + +## 📋 DESCRIPCIÓN + +Base de datos del sistema de administración de obra e INFONAVIT. + +**Schemas principales:** +- `auth_management` - Autenticación y usuarios +- `project_management` - Proyectos y estructura de obra +- `financial_management` - Presupuestos y control financiero +- `purchasing_management` - Compras e inventarios +- `construction_management` - Control de obra y avances +- `quality_management` - Calidad y postventa +- `infonavit_management` - Integración INFONAVIT + +--- + +## 🏗️ ESTRUCTURA + +``` +ddl/ +├── 00-init.sql # Inicialización + extensiones +└── schemas/ # Schemas por contexto + ├── auth_management/ + │ ├── tables/ # Tablas (01-users.sql, 02-roles.sql, ...) + │ ├── functions/ # Funciones SQL + │ ├── triggers/ # Triggers + │ └── views/ # Vistas + ├── project_management/ + │ └── ... + └── [otros schemas]/ + +seeds/ +├── dev/ # Datos de desarrollo +└── prod/ # Datos de producción inicial + +migrations/ # Migraciones versionadas +scripts/ # Scripts de utilidad +``` + +--- + +## 🚀 SETUP INICIAL + +### 1. Crear Base de Datos + +```bash +# Ejecutar script de creación +cd scripts +./create-database.sh +``` + +### 2. Ejecutar DDL + +```bash +# Ejecutar inicialización +psql $DATABASE_URL -f ddl/00-init.sql + +# Ejecutar schemas (en orden) +psql $DATABASE_URL -f ddl/schemas/auth_management/tables/01-users.sql +# ... etc +``` + +### 3. Cargar Seeds (desarrollo) + +```bash +psql $DATABASE_URL -f seeds/dev/01-users.sql +psql $DATABASE_URL -f seeds/dev/02-projects.sql +``` + +--- + +## 🔧 SCRIPTS DISPONIBLES + +| Script | Descripción | +|--------|-------------| +| `create-database.sh` | Crea la base de datos desde cero | +| `reset-database.sh` | Elimina y recrea la base de datos | +| `run-migrations.sh` | Ejecuta migraciones pendientes | +| `backup-database.sh` | Crea backup de la base de datos | + +--- + +## 📊 CONVENCIONES + +### Nomenclatura + +Seguir **ESTANDARES-NOMENCLATURA.md**: +- Schemas: `snake_case` + sufijo `_management` +- Tablas: `snake_case` plural +- Columnas: `snake_case` singular +- Índices: `idx_{tabla}_{columnas}` +- Foreign Keys: `fk_{origen}_to_{destino}` +- Constraints: `chk_{tabla}_{columna}` + +### Estructura de Archivo DDL + +```sql +-- ============================================================================ +-- Tabla: nombre_tabla +-- Schema: nombre_schema +-- Descripción: [descripción] +-- Autor: Database-Agent +-- Fecha: YYYY-MM-DD +-- ============================================================================ + +DROP TABLE IF EXISTS schema.tabla CASCADE; + +CREATE TABLE schema.tabla ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + -- columnas... + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +COMMENT ON TABLE schema.tabla IS '[descripción]'; +COMMENT ON COLUMN schema.tabla.columna IS '[descripción]'; + +CREATE INDEX idx_tabla_columna ON schema.tabla(columna); +``` + +--- + +## 🔍 VALIDACIÓN + +### Verificar Schemas + +```sql +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name LIKE '%_management'; +``` + +### Verificar Tablas + +```sql +SELECT table_schema, table_name, + (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = t.table_schema + AND table_name = t.table_name) as column_count +FROM information_schema.tables t +WHERE table_schema LIKE '%_management' +ORDER BY table_schema, table_name; +``` + +### Verificar Índices + +```sql +SELECT tablename, indexname +FROM pg_indexes +WHERE schemaname LIKE '%_management' +ORDER BY tablename; +``` + +--- + +## 📚 REFERENCIAS + +- [DIRECTIVA-DISENO-BASE-DATOS.md](../../orchestration/directivas/DIRECTIVA-DISENO-BASE-DATOS.md) +- [ESTANDARES-NOMENCLATURA.md](../../orchestration/directivas/ESTANDARES-NOMENCLATURA.md) +- [MVP-APP.md](../../docs/00-overview/MVP-APP.md) + +--- + +## 📝 VARIABLES DE ENTORNO + +```bash +DATABASE_URL=postgresql://usuario:password@localhost:5432/nombre_db +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=erp_construccion +DB_USER=postgres +DB_PASSWORD=password +``` + +--- + +**Mantenido por:** Database-Agent +**Última actualización:** 2025-11-20 diff --git a/VALIDACION-DDL-INVENTARIOS.md b/VALIDACION-DDL-INVENTARIOS.md new file mode 100644 index 0000000..4ee0bb5 --- /dev/null +++ b/VALIDACION-DDL-INVENTARIOS.md @@ -0,0 +1,323 @@ +# VALIDACION DDL vs INVENTARIOS - ERP CONSTRUCCION +**Fecha:** 2025-12-06 +**Version:** 1.0.0 +**Generado por:** Requirements-Analyst + +--- + +## RESUMEN EJECUTIVO + +| Metrica | Inventarios | DDL Real | Estado | +|---------|-------------|----------|--------| +| **Schemas** | 6 (+ 3 pendientes) | 7 implementados | DISCREPANCIA | +| **Tablas declaradas** | 57 | 65 | DISCREPANCIA | +| **HSE Schema** | "pendiente" | 58 tablas implementadas | DESACTUALIZADO | +| **ENUMs** | 22 | 89 (22 base + 67 HSE) | DESACTUALIZADO | + +### Conclusion General +Los inventarios MASTER_INVENTORY.yml y DATABASE_INVENTORY.yml estan **DESACTUALIZADOS** respecto al DDL implementado. El schema HSE con 58 tablas y 67 ENUMs ya fue implementado pero los inventarios lo marcan como "pendiente". + +--- + +## 1. ANALISIS DE OBJETOS DDL IMPLEMENTADOS + +### 1.1 Schemas Creados (7 schemas de negocio) + +| Schema | Origen | Tablas | ENUMs | Estado | +|--------|--------|--------|-------|--------| +| core | construccion | 2 | 0 | Implementado | +| core_shared | construccion | 0 | 0 | Implementado (funciones) | +| construction | construccion | 2 | 0 | Implementado | +| hr | construccion | 3 | 0 | Implementado | +| hse | construccion | 58 | 67 | **IMPLEMENTADO** | +| estimates | construccion | 0 | 0 | Schema vacio | +| infonavit | construccion | 0 | 0 | Schema vacio | + +### 1.2 Tablas por Schema (Conteo Real del DDL) + +#### core (2 tablas) +- `core.tenants` - Multi-tenancy base +- `core.users` - Usuarios base + +#### construction (2 tablas) +- `construction.proyectos` - Proyectos de desarrollo +- `construction.fraccionamientos` - Obras/fraccionamientos + +#### hr (3 tablas) +- `hr.employees` - Empleados +- `hr.puestos` - Catalogo de puestos +- `hr.employee_fraccionamientos` - Asignacion empleados a obras + +#### hse (58 tablas) - RF-MAA017-001 a RF-MAA017-008 + +**RF-MAA017-001 Gestion de Incidentes (5 tablas):** +- `hse.incidentes` +- `hse.incidente_involucrados` +- `hse.incidente_investigacion` +- `hse.incidente_acciones` +- `hse.incidente_evidencias` + +**RF-MAA017-002 Control de Capacitaciones (6 tablas):** +- `hse.capacitaciones` +- `hse.capacitacion_matriz` +- `hse.instructores` +- `hse.capacitacion_sesiones` +- `hse.capacitacion_asistentes` +- `hse.constancias_dc3` + +**RF-MAA017-003 Inspecciones de Seguridad (7 tablas):** +- `hse.tipos_inspeccion` +- `hse.checklist_items` +- `hse.programa_inspecciones` +- `hse.inspecciones` +- `hse.inspeccion_evaluaciones` +- `hse.hallazgos` +- `hse.hallazgo_evidencias` + +**RF-MAA017-004 Control de EPP (7 tablas):** +- `hse.epp_catalogo` +- `hse.epp_matriz_puesto` +- `hse.epp_asignaciones` +- `hse.epp_inspecciones` +- `hse.epp_bajas` +- `hse.epp_inventario` +- `hse.epp_movimientos` + +**RF-MAA017-005 Cumplimiento STPS (11 tablas):** +- `hse.normas_stps` +- `hse.norma_requisitos` +- `hse.cumplimiento_obra` +- `hse.comision_seguridad` +- `hse.comision_integrantes` +- `hse.comision_recorridos` +- `hse.programa_seguridad` +- `hse.programa_actividades` +- `hse.documentos_stps` +- `hse.auditorias` + +**RF-MAA017-006 Gestion Ambiental (9 tablas):** +- `hse.residuos_catalogo` +- `hse.residuos_generacion` +- `hse.almacen_temporal` +- `hse.proveedores_ambientales` +- `hse.manifiestos_residuos` +- `hse.manifiesto_detalle` +- `hse.impacto_ambiental` +- `hse.quejas_ambientales` + +**RF-MAA017-007 Permisos de Trabajo (8 tablas):** +- `hse.tipos_permiso_trabajo` +- `hse.permisos_trabajo` +- `hse.permiso_personal` +- `hse.permiso_autorizaciones` +- `hse.permiso_checklist` +- `hse.permiso_monitoreos` +- `hse.permiso_eventos` +- `hse.permiso_documentos` + +**RF-MAA017-008 Indicadores HSE (6 tablas):** +- `hse.indicadores_config` +- `hse.indicadores_meta_obra` +- `hse.indicadores_valores` +- `hse.horas_trabajadas` +- `hse.dias_sin_accidente` +- `hse.reportes_programados` +- `hse.alertas_indicadores` + +### 1.3 ENUMs Implementados (89 total) + +**ENUMs HSE (67):** +- Incidentes: `tipo_incidente`, `gravedad_incidente`, `estado_incidente`, `rol_involucrado`, `factor_causa` +- Capacitaciones: `tipo_capacitacion`, `estado_sesion` +- Inspecciones: `frecuencia`, `estado_inspeccion`, `resultado_evaluacion`, `gravedad_hallazgo`, `estado_hallazgo`, `tipo_evidencia` +- EPP: `categoria_epp`, `estado_epp`, `estado_inspeccion_epp`, `motivo_baja_epp`, `tipo_movimiento_epp` +- STPS: `estado_comision`, `rol_comision`, `representacion`, `estado_recorrido`, `estado_programa`, `tipo_actividad_programa`, `estado_actividad`, `tipo_documento_stps`, `tipo_auditoria`, `resultado_auditoria`, `estado_cumplimiento` +- Ambiental: `categoria_residuo`, `unidad_residuo`, `estado_residuo`, `estado_almacen`, `tipo_proveedor_ambiental`, `estado_manifiesto`, `tipo_impacto`, `severidad`, `probabilidad`, `nivel_riesgo`, `estado_impacto`, `origen_queja`, `tipo_queja`, `estado_queja` +- Permisos: `estado_permiso`, `rol_permiso`, `decision_autorizacion`, `momento_checklist`, `tipo_evento_permiso` +- Indicadores: `tipo_indicador`, `frecuencia_calculo`, `periodo_tipo`, `estado_semaforo`, `fuente_horas`, `tipo_reporte_hse`, `formato_reporte`, `tipo_alerta_indicador` + +--- + +## 2. DISCREPANCIAS DETECTADAS + +### 2.1 MASTER_INVENTORY.yml + +| Campo | Valor Actual | Valor Correcto | Accion | +|-------|--------------|----------------|--------| +| `metricas.database.schemas` | 6 | 7 | Actualizar a 7 | +| `metricas.database.tablas` | 57 | 65 | Actualizar a 65 | +| `metricas.database.enums` | 22 | 89 | Actualizar a 89 | +| `schemas.hse.estado` | "pendiente" | "implementado" | Actualizar | +| `schemas.hse.tablas` | 0 | 58 | Actualizar a 58 | +| `schemas.hse.ddl` | "pendiente" | "03-hse-schema-ddl.sql" | Actualizar | +| `modulos_fase_3.MAA-017.tablas` | 11 items | 58 items | Actualizar lista completa | + +### 2.2 DATABASE_INVENTORY.yml + +| Campo | Valor Actual | Valor Correcto | Accion | +|-------|--------------|----------------|--------| +| `resumen.schemas` | 6 | 7 | Actualizar | +| `resumen.tablas` | 57 | 65 | Actualizar | +| `resumen.enums` | 22 | 89 | Actualizar | +| Schema `hse` | No existe | Agregar seccion completa | FALTA | +| Tablas HSE | 0 | 58 | Agregar todas | + +### 2.3 Tablas Faltantes en Inventarios + +Las siguientes 58 tablas HSE + 2 core + 2 construction + 3 hr = 65 tablas existen en DDL pero el inventario solo declara 57: + +**FALTANTES:** +- Todas las 58 tablas del schema `hse` +- Las 2 tablas minimas de `core` (tenants, users) +- La tabla `hr.puestos` +- La tabla `hr.employee_fraccionamientos` +- Las tablas de `construction` tienen nombres distintos en inventario vs DDL: + - Inventario: `fraccionamientos` (sin proyecto_id directo) + - DDL: `proyectos` + `fraccionamientos` (con proyecto_id) + +--- + +## 3. TRAZABILIDAD RF -> DDL + +### 3.1 Modulo MAA-017 Seguridad HSE + +| RF | Nombre | Tablas DDL | Trazabilidad | +|----|--------|------------|--------------| +| RF-MAA017-001 | Gestion de Incidentes | 5 tablas | COMPLETA | +| RF-MAA017-002 | Control de Capacitaciones | 6 tablas | COMPLETA | +| RF-MAA017-003 | Inspecciones de Seguridad | 7 tablas | COMPLETA | +| RF-MAA017-004 | Control de EPP | 7 tablas | COMPLETA | +| RF-MAA017-005 | Cumplimiento STPS | 11 tablas | COMPLETA | +| RF-MAA017-006 | Gestion Ambiental | 9 tablas | COMPLETA | +| RF-MAA017-007 | Permisos de Trabajo | 8 tablas | COMPLETA | +| RF-MAA017-008 | Indicadores HSE | 7 tablas | COMPLETA | + +**Total:** 8 RFs -> 58 tablas + 67 ENUMs + +### 3.2 Otros Modulos (Inventariados pero NO implementados en DDL) + +| Modulo | Tablas Inventario | Tablas DDL | Estado | +|--------|-------------------|------------|--------| +| MAI-002 | 8 | 2 (proyectos, fraccionamientos) | PARCIAL | +| MAI-003 | 3 | 0 | SIN DDL | +| MAI-004 | 9 | 0 | SIN DDL | +| MAI-005 | 5 | 0 | SIN DDL | +| MAI-007 | 8 | 3 (employees, puestos, employee_fracc) | PARCIAL | +| MAI-008 | 8 | 0 | SIN DDL | +| MAI-009 | 5 | 0 | SIN DDL | +| MAI-010 | 1 | 0 | SIN DDL | +| MAI-011 | 7 | 0 | SIN DDL | +| MAI-012 | 3 | 0 | SIN DDL | + +--- + +## 4. POLITICA DE CARGA LIMPIA + +### 4.1 Cumplimiento + +| Check | Estado | Detalle | +|-------|--------|---------| +| No carpeta migrations/ | OK | No existe | +| No archivos fix-*.sql | OK | No existen | +| No archivos migration-*.sql | OK | No existen | +| Existe drop-and-recreate-database.sh | OK | Existe y es ejecutable | +| DDL en schemas/ | OK | 3 archivos SQL | +| Archivo init existe | OK | init-scripts/01-init-database.sql | + +**Resultado:** POLITICA CUMPLIDA (6/6 checks) + +### 4.2 Archivos DDL Actuales + +``` +database/ +├── init-scripts/ +│ └── 01-init-database.sql # Extensiones, schemas base, funciones core +├── schemas/ +│ ├── 01-construction-schema-ddl.sql # proyectos, fraccionamientos +│ ├── 02-hr-schema-ddl.sql # employees, puestos, employee_fracc +│ └── 03-hse-schema-ddl.sql # 58 tablas HSE +├── drop-and-recreate-database.sh # Script carga limpia +└── validate-clean-load-policy.sh # Validador de politica +``` + +--- + +## 5. ACCIONES REQUERIDAS + +### 5.1 Prioridad ALTA (Inventarios Desactualizados) + +1. **Actualizar MASTER_INVENTORY.yml:** + - Cambiar `schemas.hse.estado` de "pendiente" a "implementado" + - Cambiar `schemas.hse.tablas` de 0 a 58 + - Agregar `schemas.hse.ddl: 03-hse-schema-ddl.sql` + - Actualizar conteos globales + +2. **Actualizar DATABASE_INVENTORY.yml:** + - Agregar seccion completa para schema `hse` con 58 tablas + - Agregar 67 ENUMs de HSE + - Actualizar conteos en resumen + +### 5.2 Prioridad MEDIA (Completar DDL Faltante) + +Los siguientes schemas tienen tablas inventariadas pero NO implementadas: + +| Schema | Tablas Faltantes | DDL Requerido | +|--------|------------------|---------------| +| construction | 22 tablas | construction-schema-ddl.sql (expandir) | +| estimates | 8 tablas | estimates-schema-ddl.sql (crear) | +| infonavit | 8 tablas | infonavit-schema-ddl.sql (crear) | +| hr | 5 tablas | hr-schema-ddl.sql (expandir) | +| inventory | 4 tablas | inventory-ext-schema-ddl.sql (crear) | +| purchase | 5 tablas | purchase-ext-schema-ddl.sql (crear) | + +### 5.3 Prioridad BAJA (Documentacion) + +- Crear TRACEABILITY.yml por modulo cuando se implemente +- Actualizar README de database/ con estructura actual + +--- + +## 6. RESUMEN FINAL + +### Estado Actual + +``` +DDL Implementado: +├── core: 2 tablas (tenants, users) +├── construction: 2 tablas +├── hr: 3 tablas +├── hse: 58 tablas + 67 ENUMs ← IMPLEMENTADO (inventario dice "pendiente") +├── estimates: schema vacio +├── infonavit: schema vacio +├── inventory: schema vacio +└── purchase: schema vacio + +Total: 65 tablas, 89 ENUMs +``` + +### Inventarios Declaran + +``` +MASTER + DATABASE_INVENTORY: +├── construction: 24 tablas (22 sin DDL) +├── estimates: 8 tablas (sin DDL) +├── infonavit: 8 tablas (sin DDL) +├── hr: 8 tablas (5 sin DDL) +├── inventory: 4 tablas (sin DDL) +├── purchase: 5 tablas (sin DDL) +└── hse: "pendiente" (INCORRECTO - tiene 58 tablas) + +Total declarado: 57 tablas, 22 ENUMs +``` + +### Gap Analysis + +| Categoria | Inventario | DDL Real | Diferencia | +|-----------|------------|----------|------------| +| Tablas | 57 | 65 | +8 (HSE +58, otros -50) | +| ENUMs | 22 | 89 | +67 (todos HSE) | +| Schemas implementados | 6 | 7 | +1 (hse) | + +--- + +**Documento generado automaticamente como parte de la validacion de Sprint 0.** diff --git a/_MAP.md b/_MAP.md new file mode 100644 index 0000000..d0fb298 --- /dev/null +++ b/_MAP.md @@ -0,0 +1,306 @@ +# Database Map - ERP Construccion + +**Proyecto:** ERP Construccion +**Version:** 1.0 +**Ultima Actualizacion:** 2025-12-12 +**Total Schemas:** 7 +**Total Tablas:** 110 + +--- + +## NAVEGACION RAPIDA + +``` +database/ +├── _MAP.md # Este archivo (indice maestro) +├── schemas/ # DDL por schema +│ ├── 01-construction-schema-ddl.sql # 24 tablas +│ ├── 02-hr-schema-ddl.sql # 8 tablas +│ ├── 03-hse-schema-ddl.sql # 58 tablas +│ ├── 04-estimates-schema-ddl.sql # 8 tablas +│ ├── 05-infonavit-schema-ddl.sql # 8 tablas +│ ├── 06-inventory-ext-schema-ddl.sql # 4 tablas +│ └── 07-purchase-ext-schema-ddl.sql # 5 tablas +├── init-scripts/ # Scripts de inicializacion +│ └── 01-init-database.sql +├── migrations/ # Migraciones TypeORM +├── seeds/ # Datos de prueba +└── HERENCIA-ERP-CORE.md # Referencia a ERP-Core +``` + +--- + +## SCHEMAS OVERVIEW + +| # | Schema | Tablas | Descripcion | Estado | +|---|--------|--------|-------------|--------| +| 1 | `construction` | 24 | Proyectos, estructura, avances | ✅ DDL | +| 2 | `hr` | 8 | RRHH, asistencias, cuadrillas | ✅ DDL | +| 3 | `hse` | 58 | Seguridad, incidentes, EPP | ✅ DDL | +| 4 | `estimates` | 8 | Estimaciones, anticipos | ✅ DDL | +| 5 | `infonavit` | 8 | INFONAVIT, derechohabientes | ✅ DDL | +| 6 | `inventory` | 4 | Extension inventario obra | ✅ DDL | +| 7 | `purchase` | 5 | Extension compras obra | ✅ DDL | + +--- + +## DETALLE POR SCHEMA + +### 1. Schema: `construction` (24 tablas) + +**DDL:** `schemas/01-construction-schema-ddl.sql` + +#### Estructura de Proyecto (8 tablas) + +| Tabla | Descripcion | FK Principales | +|-------|-------------|----------------| +| `proyectos` | Proyectos/obras | `auth.tenants`, `auth.users` | +| `fraccionamientos` | Fraccionamientos | `proyectos` | +| `etapas` | Etapas de construccion | `fraccionamientos` | +| `manzanas` | Manzanas | `etapas` | +| `lotes` | Lotes/unidades | `manzanas`, `prototipos` | +| `torres` | Torres (vertical) | `fraccionamientos` | +| `niveles` | Niveles/pisos | `torres` | +| `departamentos` | Departamentos | `niveles`, `prototipos` | +| `prototipos` | Tipos de vivienda | `fraccionamientos` | + +#### Presupuestos (3 tablas) + +| Tabla | Descripcion | +|-------|-------------| +| `conceptos` | Catalogo de conceptos de obra | +| `presupuestos` | Presupuestos maestros | +| `presupuesto_partidas` | Partidas presupuestales | + +#### Programacion y Avances (5 tablas) + +| Tabla | Descripcion | +|-------|-------------| +| `programa_obra` | Programa general de obra | +| `programa_actividades` | Actividades programadas | +| `avances_obra` | Registro de avances | +| `fotos_avance` | Evidencias fotograficas | +| `bitacora_obra` | Bitacora de obra | + +#### Calidad (5 tablas) + +| Tabla | Descripcion | +|-------|-------------| +| `checklists` | Checklists de calidad | +| `checklist_items` | Items de checklist | +| `inspecciones` | Inspecciones de calidad | +| `inspeccion_resultados` | Resultados | +| `tickets_postventa` | Tickets de postventa | + +#### Contratos (3 tablas) + +| Tabla | Descripcion | +|-------|-------------| +| `subcontratistas` | Catalogo subcontratistas | +| `contratos` | Contratos de obra | +| `contrato_partidas` | Partidas contratadas | + +--- + +### 2. Schema: `hr` (8 tablas) + +**DDL:** `schemas/02-hr-schema-ddl.sql` + +| Tabla | Descripcion | Caracteristicas | +|-------|-------------|-----------------| +| `employees` | Empleados base | Extension de core | +| `employee_construction` | Extension construccion | Campos especificos | +| `puestos` | Catalogo de puestos | - | +| `asistencias` | Registro asistencias | GPS, biometrico | +| `asistencia_biometrico` | Datos biometricos | - | +| `geocercas` | Geocercas para GPS | PostGIS | +| `destajo` | Trabajo a destajo | - | +| `destajo_detalle` | Mediciones destajo | - | +| `cuadrillas` | Cuadrillas de trabajo | - | +| `cuadrilla_miembros` | Miembros de cuadrilla | - | +| `employee_fraccionamientos` | Asignacion a fracc | - | + +--- + +### 3. Schema: `hse` (58 tablas) + +**DDL:** `schemas/03-hse-schema-ddl.sql` + +#### Gestion de Incidentes (5 tablas) +- `incidentes`, `incidente_involucrados`, `incidente_acciones` +- `incidente_evidencias`, `incidente_causas` + +#### Control de Capacitaciones (6 tablas) +- `capacitaciones`, `capacitacion_participantes`, `capacitacion_materiales` +- `certificaciones`, `certificacion_empleados`, `plan_capacitacion` + +#### Inspecciones de Seguridad (7 tablas) +- `inspecciones_seguridad`, `inspeccion_hallazgos` +- `checklist_seguridad`, `checklist_seguridad_items` +- `areas_riesgo`, `rondas_seguridad`, `ronda_puntos` + +#### Control de EPP (7 tablas) +- `epp_catalogo`, `epp_asignaciones`, `epp_entregas` +- `epp_devoluciones`, `epp_inspecciones` +- `epp_vida_util`, `epp_stock` + +#### Cumplimiento STPS (11 tablas) +- `normas_stps`, `requisitos_norma`, `cumplimiento_norma` +- `auditorias_stps`, `auditoria_hallazgos` +- `planes_accion`, `acciones_correctivas` +- `comision_seguridad`, `comision_miembros` +- `recorridos_comision`, `actas_comision` + +#### Gestion Ambiental (9 tablas) +- `impactos_ambientales`, `residuos`, `residuo_movimientos` +- `manifiestos_residuos`, `monitoreo_ambiental` +- `permisos_ambientales`, `programas_ambientales` +- `indicadores_ambientales`, `eventos_ambientales` + +#### Permisos de Trabajo (8 tablas) +- `permisos_trabajo`, `permiso_riesgos`, `permiso_autorizaciones` +- `permisos_altura`, `permisos_caliente`, `permisos_confinado` +- `permisos_electrico`, `permisos_excavacion` + +#### Indicadores HSE (7 tablas) +- `kpi_configuracion`, `kpi_valores`, `kpi_metas` +- `dashboards_hse`, `alertas_hse` +- `reportes_hse`, `estadisticas_periodo` + +--- + +### 4. Schema: `estimates` (8 tablas) + +**DDL:** `schemas/04-estimates-schema-ddl.sql` + +| Tabla | Descripcion | +|-------|-------------| +| `estimaciones` | Estimaciones de obra | +| `estimacion_conceptos` | Conceptos estimados | +| `generadores` | Numeros generadores | +| `anticipos` | Anticipos de obra | +| `amortizaciones` | Amortizacion de anticipos | +| `retenciones` | Retenciones (garantia, IMSS) | +| `fondo_garantia` | Fondo de garantia | +| `estimacion_workflow` | Workflow de aprobacion | + +--- + +### 5. Schema: `infonavit` (8 tablas) + +**DDL:** `schemas/05-infonavit-schema-ddl.sql` + +| Tabla | Descripcion | +|-------|-------------| +| `registro_infonavit` | Registro RUV | +| `oferta_vivienda` | Oferta registrada | +| `derechohabientes` | Derechohabientes | +| `asignacion_vivienda` | Asignaciones | +| `actas` | Actas de entrega | +| `acta_viviendas` | Viviendas en acta | +| `reportes_infonavit` | Reportes RUV | +| `historico_puntos` | Historico puntos ecologicos | + +--- + +### 6. Schema: `inventory` Extension (4 tablas) + +**DDL:** `schemas/06-inventory-ext-schema-ddl.sql` + +| Tabla | Descripcion | +|-------|-------------| +| `almacenes_proyecto` | Almacenes por obra | +| `requisiciones_obra` | Requisiciones desde obra | +| `requisicion_lineas` | Lineas de requisicion | +| `consumos_obra` | Consumos por lote/concepto | + +--- + +### 7. Schema: `purchase` Extension (5 tablas) + +**DDL:** `schemas/07-purchase-ext-schema-ddl.sql` + +| Tabla | Descripcion | +|-------|-------------| +| `purchase_order_construction` | Extension OC | +| `supplier_construction` | Extension proveedores | +| `comparativo_cotizaciones` | Cuadro comparativo | +| `comparativo_proveedores` | Proveedores en comparativo | +| `comparativo_productos` | Productos cotizados | + +--- + +## ORDEN DE EJECUCION DDL + +```bash +# Prerequisito: ERP-Core debe estar instalado +# Schema auth.* y core.* deben existir + +# 1. Construction (base) +psql $DATABASE_URL -f schemas/01-construction-schema-ddl.sql + +# 2. HR (depende de construction) +psql $DATABASE_URL -f schemas/02-hr-schema-ddl.sql + +# 3. HSE (depende de construction y hr) +psql $DATABASE_URL -f schemas/03-hse-schema-ddl.sql + +# 4. Estimates (depende de construction) +psql $DATABASE_URL -f schemas/04-estimates-schema-ddl.sql + +# 5. INFONAVIT (depende de construction) +psql $DATABASE_URL -f schemas/05-infonavit-schema-ddl.sql + +# 6. Inventory Extension (depende de construction) +psql $DATABASE_URL -f schemas/06-inventory-ext-schema-ddl.sql + +# 7. Purchase Extension (depende de construction) +psql $DATABASE_URL -f schemas/07-purchase-ext-schema-ddl.sql +``` + +--- + +## RELACIONES PRINCIPALES + +``` +auth.tenants + └── construction.proyectos + └── construction.fraccionamientos + ├── construction.etapas + │ └── construction.manzanas + │ └── construction.lotes + ├── construction.torres (vertical) + │ └── construction.niveles + │ └── construction.departamentos + ├── hr.employee_fraccionamientos + │ └── hr.employees + └── hse.incidentes + └── hse.incidente_involucrados + └── hr.employees +``` + +--- + +## ENUMS UTILIZADOS + +Ver archivo: `backend/src/shared/constants/enums.constants.ts` + +Los principales enums estan definidos en: +- `PROJECT_STATUS` - Estados de proyecto +- `LOT_STATUS` - Estados de lote +- `INCIDENT_SEVERITY` - Severidad de incidentes +- `ESTIMATION_STATUS` - Estados de estimacion +- `INFONAVIT_ASSIGNMENT_STATUS` - Estados INFONAVIT + +--- + +## REFERENCIAS + +- **ERP-Core DDL:** `apps/erp-core/database/ddl/` +- **Herencia:** `HERENCIA-ERP-CORE.md` +- **Constantes SSOT:** `backend/src/shared/constants/database.constants.ts` + +--- + +**Mantenido por:** Architecture-Analyst +**Actualizacion:** Manual al agregar/modificar schemas diff --git a/drop-and-recreate-database.sh b/drop-and-recreate-database.sh new file mode 100755 index 0000000..b679265 --- /dev/null +++ b/drop-and-recreate-database.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# ============================================================================= +# DROP AND RECREATE DATABASE - ERP CONSTRUCCION +# ============================================================================= +# Script de carga limpia segun DIRECTIVA-POLITICA-CARGA-LIMPIA.md +# +# Uso: ./drop-and-recreate-database.sh [DATABASE_URL] +# Ejemplo: ./drop-and-recreate-database.sh "postgresql://user:pass@localhost:5433/erp_construccion" +# ============================================================================= + +set -e + +# Colores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuracion +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DDL_DIR="$SCRIPT_DIR/ddl" +SCHEMAS_DIR="$SCRIPT_DIR/schemas" +INIT_SCRIPTS_DIR="$SCRIPT_DIR/init-scripts" + +# Database URL (por defecto desarrollo local) +DATABASE_URL="${1:-${DATABASE_URL:-postgresql://postgres:postgres@localhost:5433/erp_construccion}}" + +# Extraer parametros de conexion +DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:]+):.*|\1|') +DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|') +DB_NAME=$(echo "$DATABASE_URL" | sed -E 's|.*/([^?]+).*|\1|') +DB_USER=$(echo "$DATABASE_URL" | sed -E 's|.*://([^:]+):.*|\1|') + +echo -e "${BLUE}=============================================================================${NC}" +echo -e "${BLUE} ERP CONSTRUCCION - Carga Limpia de Base de Datos${NC}" +echo -e "${BLUE}=============================================================================${NC}" +echo "" +echo -e "Host: ${YELLOW}$DB_HOST:$DB_PORT${NC}" +echo -e "Database: ${YELLOW}$DB_NAME${NC}" +echo -e "Usuario: ${YELLOW}$DB_USER${NC}" +echo "" + +# ============================================================================= +# PASO 1: Verificar conexion +# ============================================================================= +echo -e "${YELLOW}[1/6] Verificando conexion a PostgreSQL...${NC}" +if ! psql "$DATABASE_URL" -c "SELECT 1" > /dev/null 2>&1; then + echo -e "${RED}ERROR: No se puede conectar a PostgreSQL${NC}" + echo -e "${YELLOW}Verificar que PostgreSQL esta corriendo y las credenciales son correctas${NC}" + exit 1 +fi +echo -e "${GREEN}OK - Conexion establecida${NC}" +echo "" + +# ============================================================================= +# PASO 2: DROP schemas existentes (carga limpia) +# ============================================================================= +echo -e "${YELLOW}[2/6] Eliminando schemas existentes (carga limpia)...${NC}" + +# Lista de schemas a eliminar (orden inverso de dependencias) +SCHEMAS_TO_DROP=( + "hse" + "infonavit_management" + "safety_management" + "quality_management" + "construction_management" + "inventory_management" + "purchasing_management" + "financial_management" + "project_management" + "auth_management" + "core_shared" +) + +for schema in "${SCHEMAS_TO_DROP[@]}"; do + if psql "$DATABASE_URL" -t -c "SELECT 1 FROM pg_namespace WHERE nspname = '$schema'" 2>/dev/null | grep -q 1; then + psql "$DATABASE_URL" -c "DROP SCHEMA IF EXISTS $schema CASCADE" > /dev/null 2>&1 + echo -e " - Schema ${YELLOW}$schema${NC} eliminado" + fi +done +echo -e "${GREEN}OK - Schemas eliminados${NC}" +echo "" + +# ============================================================================= +# PASO 3: Ejecutar DDL inicial +# ============================================================================= +echo -e "${YELLOW}[3/6] Ejecutando DDL inicial (extensiones, schemas base)...${NC}" + +if [ -f "$INIT_SCRIPTS_DIR/01-init-database.sql" ]; then + psql "$DATABASE_URL" -f "$INIT_SCRIPTS_DIR/01-init-database.sql" > /dev/null 2>&1 + echo -e "${GREEN}OK - DDL inicial ejecutado${NC}" +elif [ -f "$DDL_DIR/00-init.sql" ]; then + psql "$DATABASE_URL" -f "$DDL_DIR/00-init.sql" > /dev/null 2>&1 + echo -e "${GREEN}OK - DDL inicial ejecutado (00-init.sql)${NC}" +else + echo -e "${RED}ERROR: No se encontro archivo de inicializacion${NC}" + exit 1 +fi +echo "" + +# ============================================================================= +# PASO 4: Ejecutar DDL de schemas modulares +# ============================================================================= +echo -e "${YELLOW}[4/6] Ejecutando DDL de schemas modulares...${NC}" + +# Buscar y ejecutar todos los archivos DDL en orden +DDL_FILES=$(find "$SCHEMAS_DIR" -name "*.sql" -type f 2>/dev/null | sort) + +if [ -z "$DDL_FILES" ]; then + echo -e "${YELLOW} No hay archivos DDL modulares adicionales${NC}" +else + for ddl_file in $DDL_FILES; do + filename=$(basename "$ddl_file") + echo -ne " - Ejecutando ${YELLOW}$filename${NC}..." + if psql "$DATABASE_URL" -f "$ddl_file" > /dev/null 2>&1; then + echo -e " ${GREEN}OK${NC}" + else + echo -e " ${RED}ERROR${NC}" + echo -e "${RED}Fallo al ejecutar: $ddl_file${NC}" + exit 1 + fi + done +fi +echo "" + +# ============================================================================= +# PASO 5: Ejecutar DDL legacy (si existe) +# ============================================================================= +echo -e "${YELLOW}[5/6] Ejecutando DDL de schemas legacy (si existen)...${NC}" + +LEGACY_DDL_DIR="$DDL_DIR/schemas" +if [ -d "$LEGACY_DDL_DIR" ]; then + LEGACY_FILES=$(find "$LEGACY_DDL_DIR" -name "*.sql" -type f 2>/dev/null | sort) + for ddl_file in $LEGACY_FILES; do + filename=$(basename "$ddl_file") + dirname=$(dirname "$ddl_file" | xargs basename) + echo -ne " - ${YELLOW}$dirname/$filename${NC}..." + if psql "$DATABASE_URL" -f "$ddl_file" > /dev/null 2>&1; then + echo -e " ${GREEN}OK${NC}" + else + echo -e " ${YELLOW}SKIP (puede requerir dependencias)${NC}" + fi + done +else + echo -e " No hay DDL legacy" +fi +echo "" + +# ============================================================================= +# PASO 6: Verificar resultado +# ============================================================================= +echo -e "${YELLOW}[6/6] Verificando resultado...${NC}" +echo "" + +# Contar schemas creados +SCHEMA_COUNT=$(psql "$DATABASE_URL" -t -c " +SELECT COUNT(*) FROM pg_namespace +WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1', 'public') +AND nspname NOT LIKE 'pg_%' +" | tr -d ' ') + +# Contar tablas totales +TABLE_COUNT=$(psql "$DATABASE_URL" -t -c " +SELECT COUNT(*) FROM pg_tables +WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public') +" | tr -d ' ') + +# Mostrar resumen por schema +echo -e "${GREEN}=== RESUMEN DE CARGA LIMPIA ===${NC}" +echo "" +psql "$DATABASE_URL" -c " +SELECT + schemaname AS \"Schema\", + COUNT(*) AS \"Tablas\" +FROM pg_tables +WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public') +GROUP BY schemaname +ORDER BY schemaname; +" + +echo "" +echo -e "${GREEN}=============================================================================${NC}" +echo -e "${GREEN} CARGA LIMPIA COMPLETADA EXITOSAMENTE${NC}" +echo -e "${GREEN}=============================================================================${NC}" +echo -e " Schemas creados: ${YELLOW}$SCHEMA_COUNT${NC}" +echo -e " Tablas creadas: ${YELLOW}$TABLE_COUNT${NC}" +echo -e "${GREEN}=============================================================================${NC}" diff --git a/init-scripts/01-init-database.sql b/init-scripts/01-init-database.sql new file mode 100644 index 0000000..48fa162 --- /dev/null +++ b/init-scripts/01-init-database.sql @@ -0,0 +1,317 @@ +-- ============================================================================ +-- Archivo: 01-init-database.sql +-- Descripcion: Inicializacion completa de base de datos - Carga Limpia +-- Proyecto: ERP Suite - Vertical Construccion +-- Version: 2.0.0 +-- Fecha: 2025-12-06 +-- ============================================================================ +-- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) +-- Este archivo es parte de la fuente de verdad DDL. +-- NO usar migrations, fixes o patches incrementales. +-- ============================================================================ + +-- ============================================================================ +-- EXTENSIONES +-- ============================================================================ + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Generacion de UUIDs +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Funciones criptograficas +CREATE EXTENSION IF NOT EXISTS "postgis"; -- Geolocalizacion +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Busqueda fuzzy +CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- Indices GiST avanzados + +-- ============================================================================ +-- SCHEMA: core_shared (funciones compartidas) +-- ============================================================================ + +CREATE SCHEMA IF NOT EXISTS core_shared; + +COMMENT ON SCHEMA core_shared IS 'Funciones, tipos y utilidades compartidas entre modulos'; + +-- ============================================================================ +-- FUNCIONES DE AUDITORIA +-- ============================================================================ + +-- Funcion para actualizar updated_at automaticamente +CREATE OR REPLACE FUNCTION core_shared.set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION core_shared.set_updated_at() IS +'Trigger function para actualizar automaticamente el campo updated_at en cada UPDATE'; + +-- Funcion para establecer tenant_id desde contexto +CREATE OR REPLACE FUNCTION core_shared.set_tenant_id() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.tenant_id IS NULL THEN + NEW.tenant_id = current_setting('app.current_tenant_id', true)::uuid; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION core_shared.set_tenant_id() IS +'Trigger function para establecer tenant_id automaticamente desde el contexto de sesion'; + +-- Funcion para establecer created_by desde contexto +CREATE OR REPLACE FUNCTION core_shared.set_created_by() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.created_by IS NULL THEN + NEW.created_by = current_setting('app.current_user_id', true)::uuid; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION core_shared.set_created_by() IS +'Trigger function para establecer created_by automaticamente desde el contexto de sesion'; + +-- ============================================================================ +-- FUNCIONES DE CONTEXTO +-- ============================================================================ + +CREATE OR REPLACE FUNCTION core_shared.get_current_tenant_id() +RETURNS UUID AS $$ +BEGIN + RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; +EXCEPTION + WHEN OTHERS THEN + RETURN NULL; +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION core_shared.get_current_tenant_id() IS +'Obtiene el ID del tenant actual desde el contexto de sesion'; + +CREATE OR REPLACE FUNCTION core_shared.get_current_user_id() +RETURNS UUID AS $$ +BEGIN + RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID; +EXCEPTION + WHEN OTHERS THEN + RETURN NULL; +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION core_shared.get_current_user_id() IS +'Obtiene el ID del usuario actual desde el contexto de sesion'; + +-- ============================================================================ +-- FUNCIONES DE UTILIDAD +-- ============================================================================ + +-- Generar slug desde texto +CREATE OR REPLACE FUNCTION core_shared.generate_slug(input_text TEXT) +RETURNS TEXT AS $$ +BEGIN + RETURN LOWER( + REGEXP_REPLACE( + REGEXP_REPLACE( + TRIM(input_text), + '[^a-zA-Z0-9\s-]', '', 'g' + ), + '\s+', '-', 'g' + ) + ); +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Validar formato de email +CREATE OR REPLACE FUNCTION core_shared.is_valid_email(email TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Validar formato de RFC mexicano +CREATE OR REPLACE FUNCTION core_shared.is_valid_rfc(rfc TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN rfc ~* '^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$'; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- ============================================================================ +-- PERMISOS +-- ============================================================================ + +GRANT USAGE ON SCHEMA core_shared TO PUBLIC; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA core_shared TO PUBLIC; + +-- ============================================================================ +-- SCHEMAS DE NEGOCIO - NOMENCLATURA UNIFICADA +-- ============================================================================ +-- Segun NAMING-CONVENTIONS.md, usamos nombres cortos y descriptivos: +-- - construction (antes project_management, construction_management) +-- - estimates (antes financial_management) +-- - infonavit (antes infonavit_management) +-- - hr (extension de erp-core) +-- - inventory (extension de erp-core) +-- - purchase (extension de erp-core) +-- - hse (nuevo: seguridad, salud, medio ambiente) +-- ============================================================================ + +-- Schemas propios de construccion +CREATE SCHEMA IF NOT EXISTS construction; +CREATE SCHEMA IF NOT EXISTS estimates; +CREATE SCHEMA IF NOT EXISTS infonavit; +CREATE SCHEMA IF NOT EXISTS hse; + +-- Schemas de extension (extendemos modulos de erp-core) +-- Estos pueden ya existir si erp-core esta instalado +CREATE SCHEMA IF NOT EXISTS hr; +CREATE SCHEMA IF NOT EXISTS inventory; +CREATE SCHEMA IF NOT EXISTS purchase; + +-- Schemas legacy (compatibilidad temporal, marcar para deprecacion) +-- NOTA: Estos seran eliminados en version futura +CREATE SCHEMA IF NOT EXISTS auth_management; +CREATE SCHEMA IF NOT EXISTS project_management; +CREATE SCHEMA IF NOT EXISTS financial_management; +CREATE SCHEMA IF NOT EXISTS purchasing_management; +CREATE SCHEMA IF NOT EXISTS inventory_management; +CREATE SCHEMA IF NOT EXISTS construction_management; +CREATE SCHEMA IF NOT EXISTS quality_management; +CREATE SCHEMA IF NOT EXISTS safety_management; +CREATE SCHEMA IF NOT EXISTS infonavit_management; + +-- ============================================================================ +-- COMENTARIOS EN SCHEMAS +-- ============================================================================ + +-- Schemas principales (nuevos) +COMMENT ON SCHEMA construction IS + 'Gestion de obras: proyectos, fraccionamientos, fases, viviendas, avances'; + +COMMENT ON SCHEMA estimates IS + 'Presupuestos, partidas, estimaciones, control de costos'; + +COMMENT ON SCHEMA infonavit IS + 'Integracion INFONAVIT: tramites, ECUVE, subsidios, normatividad'; + +COMMENT ON SCHEMA hse IS + 'Seguridad, Salud Ocupacional y Medio Ambiente (HSE/EHS)'; + +COMMENT ON SCHEMA hr IS + 'Extension de RRHH para construccion: cuadrillas, destajo, asistencia obra'; + +COMMENT ON SCHEMA inventory IS + 'Extension de inventarios: almacenes de obra, control de materiales'; + +COMMENT ON SCHEMA purchase IS + 'Extension de compras: proveedores de construccion, requisiciones de obra'; + +-- Schemas legacy (deprecated) +COMMENT ON SCHEMA auth_management IS + 'DEPRECATED: Usar core.auth. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA project_management IS + 'DEPRECATED: Usar construction. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA financial_management IS + 'DEPRECATED: Usar estimates. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA purchasing_management IS + 'DEPRECATED: Usar purchase. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA inventory_management IS + 'DEPRECATED: Usar inventory. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA construction_management IS + 'DEPRECATED: Usar construction. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA quality_management IS + 'DEPRECATED: Funcionalidad movida a hse (inspecciones) y construction (calidad)'; + +COMMENT ON SCHEMA safety_management IS + 'DEPRECATED: Usar hse. Schema mantenido para compatibilidad'; + +COMMENT ON SCHEMA infonavit_management IS + 'DEPRECATED: Usar infonavit. Schema mantenido para compatibilidad'; + +-- ============================================================================ +-- FUNCION LEGACY (compatibilidad) +-- ============================================================================ + +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION update_updated_at_column() IS + 'LEGACY: Usar core_shared.set_updated_at() en nuevas tablas'; + +-- ============================================================================ +-- TABLAS CORE MINIMAS (si erp-core no esta instalado) +-- ============================================================================ +-- Estas tablas son requeridas como FK por los modulos de construccion +-- Si erp-core esta instalado, estas ya existiran y el IF NOT EXISTS las omitira + +-- Schema core para tablas compartidas +CREATE SCHEMA IF NOT EXISTS core; + +-- Tabla de tenants (multi-tenancy) +CREATE TABLE IF NOT EXISTS core.tenants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + settings JSONB DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla de usuarios +CREATE TABLE IF NOT EXISTS core.users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES core.tenants(id), + email VARCHAR(255) NOT NULL, + username VARCHAR(100), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(tenant_id, email) +); + +-- ============================================================================ +-- VERIFICACION +-- ============================================================================ + +DO $$ +DECLARE + schema_count INTEGER; + ext_count INTEGER; +BEGIN + SELECT COUNT(*) INTO schema_count + FROM pg_namespace + WHERE nspname IN ('construction', 'estimates', 'infonavit', 'hse', 'hr', 'inventory', 'purchase', 'core', 'core_shared'); + + SELECT COUNT(*) INTO ext_count + FROM pg_extension + WHERE extname IN ('uuid-ossp', 'pgcrypto', 'postgis', 'pg_trgm', 'btree_gist'); + + RAISE NOTICE '============================================================'; + RAISE NOTICE 'ERP CONSTRUCCION - Base de datos inicializada'; + RAISE NOTICE '============================================================'; + RAISE NOTICE 'Extensiones instaladas: %', ext_count; + RAISE NOTICE 'Schemas principales creados: %', schema_count; + RAISE NOTICE '============================================================'; + RAISE NOTICE 'Schemas principales: construction, estimates, infonavit, hse'; + RAISE NOTICE 'Schemas extension: hr, inventory, purchase'; + RAISE NOTICE 'Schemas compartidos: core, core_shared'; + RAISE NOTICE '============================================================'; +END $$; + +-- ============================================================================ +-- FIN +-- ============================================================================ diff --git a/schemas/01-construction-schema-ddl.sql b/schemas/01-construction-schema-ddl.sql new file mode 100644 index 0000000..470ebf9 --- /dev/null +++ b/schemas/01-construction-schema-ddl.sql @@ -0,0 +1,903 @@ +-- ============================================================================ +-- 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 +-- ============================================================================ diff --git a/schemas/02-hr-schema-ddl.sql b/schemas/02-hr-schema-ddl.sql new file mode 100644 index 0000000..2ce3137 --- /dev/null +++ b/schemas/02-hr-schema-ddl.sql @@ -0,0 +1,156 @@ +-- ============================================================================ +-- HR Schema DDL - Extension de RRHH para Construccion +-- Modulo: MAI-007 RRHH y Asistencias +-- Version: 1.0.0 +-- Fecha: 2025-12-06 +-- ============================================================================ +-- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) +-- Este archivo es parte de la fuente de verdad DDL. +-- ============================================================================ + +-- Verificar prerequisitos +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN + RAISE EXCEPTION 'Schema auth no existe. ERP-Core debe estar instalado'; + 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_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero 01-construction-schema-ddl.sql'; + END IF; +END $$; + +-- Crear schema si no existe +CREATE SCHEMA IF NOT EXISTS hr; + +-- Configurar search_path +SET search_path TO hr, construction, core, core_shared, public; + +-- ============================================================================ +-- TABLAS BASE (requeridas por HSE y otros modulos) +-- ============================================================================ + +-- Tabla: Empleados +CREATE TABLE IF NOT EXISTS hr.employees ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(100) NOT NULL, + apellido_paterno VARCHAR(100) NOT NULL, + apellido_materno VARCHAR(100), + curp VARCHAR(18), + rfc VARCHAR(13), + nss VARCHAR(11), + fecha_nacimiento DATE, + genero VARCHAR(1), + email VARCHAR(255), + telefono VARCHAR(20), + direccion TEXT, + fecha_ingreso DATE NOT NULL, + fecha_baja DATE, + puesto_id UUID, + departamento VARCHAR(100), + tipo_contrato VARCHAR(50), + salario_diario DECIMAL(10,2), + estado VARCHAR(20) NOT NULL DEFAULT 'activo', + foto_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_employees_codigo UNIQUE (tenant_id, codigo), + CONSTRAINT uq_employees_curp UNIQUE (tenant_id, curp) +); + +-- Tabla: Puestos +CREATE TABLE IF NOT EXISTS hr.puestos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(100) NOT NULL, + descripcion TEXT, + nivel_riesgo VARCHAR(20), + requiere_capacitacion_especial BOOLEAN DEFAULT false, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_puestos_codigo UNIQUE (tenant_id, codigo) +); + +-- Agregar FK de puesto a empleados +ALTER TABLE hr.employees + ADD CONSTRAINT fk_employees_puesto + FOREIGN KEY (puesto_id) REFERENCES hr.puestos(id); + +-- Tabla: Asignacion de empleados a obras +CREATE TABLE IF NOT EXISTS hr.employee_fraccionamientos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + employee_id UUID NOT NULL REFERENCES hr.employees(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + fecha_inicio DATE NOT NULL, + fecha_fin DATE, + rol VARCHAR(50), + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_employee_fraccionamiento UNIQUE (employee_id, fraccionamiento_id, fecha_inicio) +); + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_employees_tenant ON hr.employees(tenant_id); +CREATE INDEX IF NOT EXISTS idx_employees_estado ON hr.employees(estado); +CREATE INDEX IF NOT EXISTS idx_employees_puesto ON hr.employees(puesto_id); +CREATE INDEX IF NOT EXISTS idx_puestos_tenant ON hr.puestos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_employee_fraccionamientos_employee ON hr.employee_fraccionamientos(employee_id); +CREATE INDEX IF NOT EXISTS idx_employee_fraccionamientos_fraccionamiento ON hr.employee_fraccionamientos(fraccionamiento_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY +-- ============================================================================ + +ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.puestos ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.employee_fraccionamientos ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenant_isolation_employees ON hr.employees + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_puestos ON hr.puestos + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_employee_fraccionamientos ON hr.employee_fraccionamientos + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- TRIGGERS +-- ============================================================================ + +CREATE TRIGGER trg_employees_updated_at + BEFORE UPDATE ON hr.employees + FOR EACH ROW EXECUTE FUNCTION core_shared.set_updated_at(); + +CREATE TRIGGER trg_puestos_updated_at + BEFORE UPDATE ON hr.puestos + FOR EACH ROW EXECUTE FUNCTION core_shared.set_updated_at(); + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON TABLE hr.employees IS 'Empleados de la empresa'; +COMMENT ON TABLE hr.puestos IS 'Catalogo de puestos de trabajo'; +COMMENT ON TABLE hr.employee_fraccionamientos IS 'Asignacion de empleados a obras/fraccionamientos'; + +-- ============================================================================ +-- FIN +-- ============================================================================ diff --git a/schemas/03-hse-schema-ddl.sql b/schemas/03-hse-schema-ddl.sql new file mode 100644 index 0000000..4fe65cf --- /dev/null +++ b/schemas/03-hse-schema-ddl.sql @@ -0,0 +1,1268 @@ +-- ============================================================================ +-- HSE Schema DDL - Seguridad, Salud Ocupacional y Medio Ambiente +-- Modulo: MAA-017 Seguridad HSE +-- Version: 1.0.0 +-- Fecha: 2025-12-06 +-- ============================================================================ + +-- Verificar prerequisitos +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'core') THEN + RAISE EXCEPTION 'Schema core no existe. Ejecutar primero erp-core DDL.'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction-schema-ddl.sql'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'hr') THEN + RAISE EXCEPTION 'Schema hr no existe. Ejecutar primero hr-schema-ddl.sql'; + END IF; +END $$; + +-- Crear schema HSE +CREATE SCHEMA IF NOT EXISTS hse; + +-- Configurar search_path +SET search_path TO hse, core, construction, hr, public; + +-- ============================================================================ +-- EXTENSION PostGIS para geolocalizacion +-- ============================================================================ +CREATE EXTENSION IF NOT EXISTS postgis; + +-- ============================================================================ +-- TIPOS ENUMERADOS +-- ============================================================================ + +-- Tipos de incidentes +CREATE TYPE hse.tipo_incidente AS ENUM ('accidente', 'incidente', 'casi_accidente'); +CREATE TYPE hse.gravedad_incidente AS ENUM ('leve', 'moderado', 'grave', 'fatal'); +CREATE TYPE hse.estado_incidente AS ENUM ('abierto', 'en_investigacion', 'cerrado'); +CREATE TYPE hse.rol_involucrado AS ENUM ('lesionado', 'testigo', 'responsable'); +CREATE TYPE hse.factor_causa AS ENUM ('acto_inseguro', 'condicion_insegura'); + +-- Tipos de capacitaciones +CREATE TYPE hse.tipo_capacitacion AS ENUM ('induccion', 'especifica', 'certificacion', 'reentrenamiento'); +CREATE TYPE hse.estado_sesion AS ENUM ('programada', 'en_curso', 'completada', 'cancelada'); + +-- Tipos de inspecciones +CREATE TYPE hse.frecuencia AS ENUM ('diaria', 'semanal', 'quincenal', 'mensual', 'eventual'); +CREATE TYPE hse.estado_inspeccion AS ENUM ('programada', 'en_progreso', 'completada', 'cancelada', 'vencida'); +CREATE TYPE hse.resultado_evaluacion AS ENUM ('cumple', 'no_cumple', 'no_aplica'); +CREATE TYPE hse.gravedad_hallazgo AS ENUM ('critico', 'mayor', 'menor'); +CREATE TYPE hse.estado_hallazgo AS ENUM ('abierto', 'en_correccion', 'verificando', 'cerrado', 'reabierto'); +CREATE TYPE hse.tipo_evidencia AS ENUM ('hallazgo', 'correccion'); + +-- Tipos de EPP +CREATE TYPE hse.categoria_epp AS ENUM ('cabeza', 'ojos', 'auditiva', 'respiratoria', 'manos', 'pies', 'caidas', 'ropa'); +CREATE TYPE hse.estado_epp AS ENUM ('activo', 'vencido', 'danado', 'perdido', 'devuelto'); +CREATE TYPE hse.estado_inspeccion_epp AS ENUM ('bueno', 'regular', 'malo', 'danado'); +CREATE TYPE hse.motivo_baja_epp AS ENUM ('vencimiento', 'danado', 'perdido', 'terminacion_laboral'); +CREATE TYPE hse.tipo_movimiento_epp AS ENUM ('entrada', 'salida', 'transferencia', 'ajuste'); + +-- Tipos STPS +CREATE TYPE hse.estado_comision AS ENUM ('activa', 'vencida', 'renovada'); +CREATE TYPE hse.rol_comision AS ENUM ('presidente', 'secretario', 'vocal_patronal', 'vocal_trabajador'); +CREATE TYPE hse.representacion AS ENUM ('patronal', 'trabajadores'); +CREATE TYPE hse.estado_recorrido AS ENUM ('programado', 'realizado', 'cancelado', 'pendiente'); +CREATE TYPE hse.estado_programa AS ENUM ('borrador', 'activo', 'finalizado'); +CREATE TYPE hse.tipo_actividad_programa AS ENUM ('capacitacion', 'inspeccion', 'simulacro', 'campana', 'otro'); +CREATE TYPE hse.estado_actividad AS ENUM ('pendiente', 'en_progreso', 'completada', 'cancelada'); +CREATE TYPE hse.tipo_documento_stps AS ENUM ('dc1', 'dc2', 'dc3', 'dc4', 'st7', 'st9'); +CREATE TYPE hse.tipo_auditoria AS ENUM ('interna', 'simulada', 'stps', 'cliente', 'certificadora'); +CREATE TYPE hse.resultado_auditoria AS ENUM ('aprobada', 'aprobada_observaciones', 'no_aprobada'); +CREATE TYPE hse.estado_cumplimiento AS ENUM ('cumple', 'parcial', 'no_cumple', 'no_aplica'); + +-- Tipos ambientales +CREATE TYPE hse.categoria_residuo AS ENUM ('peligroso', 'manejo_especial', 'urbano'); +CREATE TYPE hse.unidad_residuo AS ENUM ('kg', 'litros', 'm3', 'piezas'); +CREATE TYPE hse.estado_residuo AS ENUM ('almacenado', 'en_transito', 'dispuesto'); +CREATE TYPE hse.estado_almacen AS ENUM ('operativo', 'lleno', 'mantenimiento'); +CREATE TYPE hse.tipo_proveedor_ambiental AS ENUM ('transportista', 'reciclador', 'confinamiento'); +CREATE TYPE hse.estado_manifiesto AS ENUM ('emitido', 'en_transito', 'entregado', 'cerrado'); +CREATE TYPE hse.tipo_impacto AS ENUM ('ruido', 'polvo', 'vibraciones', 'agua', 'emision', 'vegetacion', 'otro'); +CREATE TYPE hse.severidad AS ENUM ('bajo', 'medio', 'alto'); +CREATE TYPE hse.probabilidad AS ENUM ('baja', 'media', 'alta'); +CREATE TYPE hse.nivel_riesgo AS ENUM ('tolerable', 'moderado', 'significativo'); +CREATE TYPE hse.estado_impacto AS ENUM ('identificado', 'mitigando', 'controlado'); +CREATE TYPE hse.origen_queja AS ENUM ('vecino', 'autoridad', 'interno', 'anonimo'); +CREATE TYPE hse.tipo_queja AS ENUM ('ruido', 'polvo', 'olores', 'agua', 'otro'); +CREATE TYPE hse.estado_queja AS ENUM ('recibida', 'atendiendo', 'cerrada'); + +-- Tipos permisos de trabajo +CREATE TYPE hse.estado_permiso AS ENUM ('borrador', 'solicitado', 'aprobado_parcial', 'autorizado', 'en_ejecucion', 'suspendido', 'cerrado', 'rechazado', 'vencido'); +CREATE TYPE hse.rol_permiso AS ENUM ('ejecutor', 'supervisor', 'vigia', 'operador', 'senalero'); +CREATE TYPE hse.decision_autorizacion AS ENUM ('aprobado', 'rechazado'); +CREATE TYPE hse.momento_checklist AS ENUM ('pre_trabajo', 'durante', 'post_trabajo'); +CREATE TYPE hse.tipo_evento_permiso AS ENUM ('inicio', 'suspension', 'reanudacion', 'extension', 'anomalia', 'cierre'); + +-- Tipos indicadores +CREATE TYPE hse.tipo_indicador AS ENUM ('reactivo', 'proactivo', 'ambiental'); +CREATE TYPE hse.frecuencia_calculo AS ENUM ('diario', 'semanal', 'mensual'); +CREATE TYPE hse.periodo_tipo AS ENUM ('diario', 'semanal', 'mensual', 'anual'); +CREATE TYPE hse.estado_semaforo AS ENUM ('verde', 'amarillo', 'rojo'); +CREATE TYPE hse.fuente_horas AS ENUM ('asistencia', 'manual'); +CREATE TYPE hse.tipo_reporte_hse AS ENUM ('semanal', 'mensual', 'trimestral', 'anual'); +CREATE TYPE hse.formato_reporte AS ENUM ('pdf', 'excel', 'ambos'); +CREATE TYPE hse.tipo_alerta_indicador AS ENUM ('meta_superada', 'tendencia_negativa', 'sin_datos'); + +-- ============================================================================ +-- RF-MAA017-001: GESTION DE INCIDENTES +-- ============================================================================ + +-- Tabla: Incidentes +CREATE TABLE hse.incidentes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + folio VARCHAR(20) NOT NULL, + fecha_hora TIMESTAMPTZ NOT NULL, + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + ubicacion_descripcion TEXT, + ubicacion_geo GEOMETRY(Point, 4326), + tipo hse.tipo_incidente NOT NULL, + gravedad hse.gravedad_incidente NOT NULL, + descripcion TEXT NOT NULL, + causa_inmediata TEXT, + causa_basica TEXT, + estado hse.estado_incidente NOT NULL DEFAULT 'abierto', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_incidentes_folio UNIQUE (tenant_id, folio) +); + +-- Tabla: Involucrados en incidentes +CREATE TABLE hse.incidente_involucrados ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, + employee_id UUID NOT NULL REFERENCES hr.employees(id), + rol hse.rol_involucrado NOT NULL, + descripcion_lesion TEXT, + parte_cuerpo VARCHAR(100), + dias_incapacidad INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Investigacion de incidentes +CREATE TABLE hse.incidente_investigacion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, + fecha_inicio DATE NOT NULL, + fecha_cierre DATE, + investigador_id UUID REFERENCES hr.employees(id), + metodologia VARCHAR(100), + factor_causa hse.factor_causa, + analisis_causas TEXT, + conclusiones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Acciones correctivas de incidentes +CREATE TABLE hse.incidente_acciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, + descripcion TEXT NOT NULL, + tipo VARCHAR(50) NOT NULL, + responsable_id UUID REFERENCES hr.employees(id), + fecha_compromiso DATE NOT NULL, + fecha_cierre DATE, + estado VARCHAR(20) NOT NULL DEFAULT 'pendiente', + evidencia_url VARCHAR(500), + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Evidencias de incidentes (fotos) +CREATE TABLE hse.incidente_evidencias ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, + tipo VARCHAR(50) NOT NULL DEFAULT 'foto', + archivo_url VARCHAR(500) NOT NULL, + descripcion VARCHAR(200), + ubicacion_geo GEOMETRY(Point, 4326), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- RF-MAA017-002: CONTROL DE CAPACITACIONES +-- ============================================================================ + +-- Tabla: Catalogo de capacitaciones +CREATE TABLE hse.capacitaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + descripcion TEXT, + tipo hse.tipo_capacitacion NOT NULL, + duracion_horas DECIMAL(4,1) NOT NULL, + validez_meses INTEGER, + norma_referencia VARCHAR(50), + requiere_evaluacion BOOLEAN NOT NULL DEFAULT false, + calificacion_minima INTEGER DEFAULT 70, + contenido_tematico TEXT, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_capacitaciones_codigo UNIQUE (tenant_id, codigo) +); + +-- Tabla: Matriz de capacitacion por puesto +CREATE TABLE hse.capacitacion_matriz ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + puesto_id UUID NOT NULL, + capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), + es_obligatoria BOOLEAN NOT NULL DEFAULT true, + plazo_dias INTEGER DEFAULT 30, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Instructores +CREATE TABLE hse.instructores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + nombre VARCHAR(200) NOT NULL, + registro_stps VARCHAR(50), + especialidades TEXT, + es_interno BOOLEAN NOT NULL DEFAULT false, + employee_id UUID REFERENCES hr.employees(id), + contacto_telefono VARCHAR(20), + contacto_email VARCHAR(100), + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Sesiones de capacitacion +CREATE TABLE hse.capacitacion_sesiones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + instructor_id UUID REFERENCES hse.instructores(id), + fecha_programada DATE NOT NULL, + hora_inicio TIME NOT NULL, + hora_fin TIME NOT NULL, + lugar VARCHAR(200), + cupo_maximo INTEGER, + estado hse.estado_sesion NOT NULL DEFAULT 'programada', + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Asistencia a capacitaciones +CREATE TABLE hse.capacitacion_asistentes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sesion_id UUID NOT NULL REFERENCES hse.capacitacion_sesiones(id) ON DELETE CASCADE, + employee_id UUID NOT NULL REFERENCES hr.employees(id), + asistio BOOLEAN DEFAULT false, + hora_entrada TIME, + hora_salida TIME, + calificacion INTEGER, + aprobado BOOLEAN, + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Constancias DC-3 +CREATE TABLE hse.constancias_dc3 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + folio VARCHAR(30) NOT NULL, + asistente_id UUID NOT NULL REFERENCES hse.capacitacion_asistentes(id), + employee_id UUID NOT NULL REFERENCES hr.employees(id), + capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), + fecha_emision DATE NOT NULL, + fecha_vencimiento DATE, + documento_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_constancias_dc3_folio UNIQUE (tenant_id, folio) +); + +-- ============================================================================ +-- RF-MAA017-003: INSPECCIONES DE SEGURIDAD +-- ============================================================================ + +-- Tabla: Tipos de inspeccion +CREATE TABLE hse.tipos_inspeccion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + descripcion TEXT, + frecuencia hse.frecuencia NOT NULL, + norma_referencia VARCHAR(50), + duracion_estimada_min INTEGER, + requiere_firma BOOLEAN NOT NULL DEFAULT true, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_tipos_inspeccion_codigo UNIQUE (tenant_id, codigo) +); + +-- Tabla: Items de checklist +CREATE TABLE hse.checklist_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id) ON DELETE CASCADE, + numero_orden INTEGER NOT NULL, + categoria VARCHAR(100), + descripcion TEXT NOT NULL, + criterio_cumplimiento TEXT, + es_critico BOOLEAN NOT NULL DEFAULT false, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Programa de inspecciones +CREATE TABLE hse.programa_inspecciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id), + inspector_id UUID REFERENCES hr.employees(id), + fecha_programada DATE NOT NULL, + hora_programada TIME, + zona_area VARCHAR(200), + estado hse.estado_inspeccion NOT NULL DEFAULT 'programada', + motivo_cancelacion TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Inspecciones ejecutadas +CREATE TABLE hse.inspecciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + programa_id UUID REFERENCES hse.programa_inspecciones(id), + tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + inspector_id UUID NOT NULL REFERENCES hr.employees(id), + fecha_inicio TIMESTAMPTZ NOT NULL, + fecha_fin TIMESTAMPTZ, + ubicacion_geo GEOMETRY(Point, 4326), + items_evaluados INTEGER DEFAULT 0, + items_cumple INTEGER DEFAULT 0, + items_no_cumple INTEGER DEFAULT 0, + items_no_aplica INTEGER DEFAULT 0, + porcentaje_cumplimiento DECIMAL(5,2), + observaciones_generales TEXT, + firma_inspector TEXT, + estado VARCHAR(20) NOT NULL DEFAULT 'borrador', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Evaluaciones de inspeccion +CREATE TABLE hse.inspeccion_evaluaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + inspeccion_id UUID NOT NULL REFERENCES hse.inspecciones(id) ON DELETE CASCADE, + checklist_item_id UUID NOT NULL REFERENCES hse.checklist_items(id), + resultado hse.resultado_evaluacion NOT NULL, + observacion TEXT, + genera_hallazgo BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Hallazgos de inspeccion +CREATE TABLE hse.hallazgos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + inspeccion_id UUID NOT NULL REFERENCES hse.inspecciones(id), + evaluacion_id UUID REFERENCES hse.inspeccion_evaluaciones(id), + folio VARCHAR(20) NOT NULL, + gravedad hse.gravedad_hallazgo NOT NULL, + tipo hse.factor_causa NOT NULL, + descripcion TEXT NOT NULL, + ubicacion_descripcion VARCHAR(500), + ubicacion_geo GEOMETRY(Point, 4326), + responsable_correccion_id UUID REFERENCES hr.employees(id), + fecha_limite DATE NOT NULL, + estado hse.estado_hallazgo NOT NULL DEFAULT 'abierto', + fecha_correccion TIMESTAMPTZ, + descripcion_correccion TEXT, + verificador_id UUID REFERENCES hr.employees(id), + fecha_verificacion TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_hallazgos_folio UNIQUE (tenant_id, folio) +); + +-- Tabla: Evidencias de hallazgos +CREATE TABLE hse.hallazgo_evidencias ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + hallazgo_id UUID NOT NULL REFERENCES hse.hallazgos(id) ON DELETE CASCADE, + tipo hse.tipo_evidencia NOT NULL, + archivo_url VARCHAR(500) NOT NULL, + descripcion VARCHAR(200), + ubicacion_geo GEOMETRY(Point, 4326), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- RF-MAA017-004: CONTROL DE EPP +-- ============================================================================ + +-- Tabla: Catalogo de EPP +CREATE TABLE hse.epp_catalogo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + categoria hse.categoria_epp NOT NULL, + descripcion TEXT, + especificaciones TEXT, + vida_util_dias INTEGER NOT NULL, + norma_referencia VARCHAR(50), + requiere_certificacion BOOLEAN NOT NULL DEFAULT false, + requiere_inspeccion_periodica BOOLEAN NOT NULL DEFAULT false, + frecuencia_inspeccion_dias INTEGER, + alerta_dias_antes INTEGER DEFAULT 15, + imagen_url VARCHAR(500), + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_epp_catalogo_codigo UNIQUE (tenant_id, codigo) +); + +-- Tabla: Matriz EPP por puesto +CREATE TABLE hse.epp_matriz_puesto ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + puesto_id UUID NOT NULL, + epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), + es_obligatorio BOOLEAN NOT NULL DEFAULT true, + actividad_especifica VARCHAR(200), + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Asignaciones de EPP +CREATE TABLE hse.epp_asignaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + employee_id UUID NOT NULL REFERENCES hr.employees(id), + epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + fecha_entrega DATE NOT NULL, + fecha_vencimiento DATE NOT NULL, + numero_serie VARCHAR(100), + numero_lote VARCHAR(100), + firma_trabajador TEXT, + foto_entrega_url VARCHAR(500), + capacitacion_uso BOOLEAN NOT NULL DEFAULT false, + estado hse.estado_epp NOT NULL DEFAULT 'activo', + costo_unitario DECIMAL(10,2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: Inspecciones de EPP +CREATE TABLE hse.epp_inspecciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + asignacion_id UUID NOT NULL REFERENCES hse.epp_asignaciones(id) ON DELETE CASCADE, + inspector_id UUID NOT NULL REFERENCES hr.employees(id), + fecha_inspeccion DATE NOT NULL, + estado_epp hse.estado_inspeccion_epp NOT NULL, + observaciones TEXT, + requiere_reemplazo BOOLEAN NOT NULL DEFAULT false, + foto_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Bajas de EPP +CREATE TABLE hse.epp_bajas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + asignacion_id UUID NOT NULL REFERENCES hse.epp_asignaciones(id), + fecha_baja DATE NOT NULL, + motivo hse.motivo_baja_epp NOT NULL, + descripcion TEXT, + descuento_aplicado BOOLEAN NOT NULL DEFAULT false, + monto_descuento DECIMAL(10,2), + autorizado_por UUID REFERENCES hr.employees(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Inventario de EPP +CREATE TABLE hse.epp_inventario ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), + almacen_id UUID, + cantidad_disponible INTEGER NOT NULL DEFAULT 0, + cantidad_minima INTEGER DEFAULT 0, + cantidad_maxima INTEGER, + costo_promedio DECIMAL(10,2), + ultima_entrada DATE, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Movimientos de EPP +CREATE TABLE hse.epp_movimientos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), + almacen_origen_id UUID, + almacen_destino_id UUID, + tipo hse.tipo_movimiento_epp NOT NULL, + cantidad INTEGER NOT NULL, + referencia VARCHAR(100), + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- RF-MAA017-005: CUMPLIMIENTO STPS +-- ============================================================================ + +-- Tabla: Catalogo de normas STPS +CREATE TABLE hse.normas_stps ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + codigo VARCHAR(30) NOT NULL UNIQUE, + nombre VARCHAR(300) NOT NULL, + descripcion TEXT, + fecha_publicacion DATE, + ultima_actualizacion DATE, + aplica_construccion BOOLEAN NOT NULL DEFAULT true, + documento_url VARCHAR(500), + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Requisitos por norma +CREATE TABLE hse.norma_requisitos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + norma_id UUID NOT NULL REFERENCES hse.normas_stps(id) ON DELETE CASCADE, + numero VARCHAR(20) NOT NULL, + descripcion TEXT NOT NULL, + tipo_evidencia VARCHAR(200), + es_critico BOOLEAN NOT NULL DEFAULT false, + aplica_a VARCHAR(100), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Cumplimiento por obra +CREATE TABLE hse.cumplimiento_obra ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + norma_id UUID NOT NULL REFERENCES hse.normas_stps(id), + requisito_id UUID REFERENCES hse.norma_requisitos(id), + estado hse.estado_cumplimiento NOT NULL DEFAULT 'no_cumple', + evidencia_url VARCHAR(500), + observaciones TEXT, + fecha_evaluacion DATE NOT NULL, + evaluador_id UUID REFERENCES hr.employees(id), + fecha_compromiso DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Comisiones de seguridad e higiene +CREATE TABLE hse.comision_seguridad ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + fecha_constitucion DATE NOT NULL, + numero_acta VARCHAR(50), + vigencia_inicio DATE NOT NULL, + vigencia_fin DATE NOT NULL, + estado hse.estado_comision NOT NULL DEFAULT 'activa', + documento_acta_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Integrantes de comision +CREATE TABLE hse.comision_integrantes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + comision_id UUID NOT NULL REFERENCES hse.comision_seguridad(id) ON DELETE CASCADE, + employee_id UUID NOT NULL REFERENCES hr.employees(id), + rol hse.rol_comision NOT NULL, + representacion hse.representacion NOT NULL, + fecha_nombramiento DATE NOT NULL, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Recorridos de comision +CREATE TABLE hse.comision_recorridos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + comision_id UUID NOT NULL REFERENCES hse.comision_seguridad(id) ON DELETE CASCADE, + fecha_programada DATE NOT NULL, + fecha_realizada DATE, + numero_acta VARCHAR(50), + areas_recorridas TEXT, + hallazgos TEXT, + recomendaciones TEXT, + estado hse.estado_recorrido NOT NULL DEFAULT 'programado', + documento_acta_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Programa de seguridad anual +CREATE TABLE hse.programa_seguridad ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + anio INTEGER NOT NULL, + objetivo_general TEXT, + metas JSONB, + presupuesto DECIMAL(12,2), + estado hse.estado_programa NOT NULL DEFAULT 'borrador', + aprobado_por UUID REFERENCES hr.employees(id), + fecha_aprobacion DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Actividades del programa +CREATE TABLE hse.programa_actividades ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + programa_id UUID NOT NULL REFERENCES hse.programa_seguridad(id) ON DELETE CASCADE, + actividad VARCHAR(300) NOT NULL, + tipo hse.tipo_actividad_programa NOT NULL, + fecha_programada DATE NOT NULL, + fecha_realizada DATE, + responsable_id UUID REFERENCES hr.employees(id), + recursos TEXT, + estado hse.estado_actividad NOT NULL DEFAULT 'pendiente', + evidencia_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Documentos STPS emitidos +CREATE TABLE hse.documentos_stps ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + tipo hse.tipo_documento_stps NOT NULL, + folio VARCHAR(30) NOT NULL, + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + employee_id UUID REFERENCES hr.employees(id), + fecha_emision DATE NOT NULL, + fecha_vencimiento DATE, + datos_documento JSONB, + documento_url VARCHAR(500), + firmado BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_documentos_stps_folio UNIQUE (tenant_id, tipo, folio) +); + +-- Tabla: Auditorias +CREATE TABLE hse.auditorias ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + tipo hse.tipo_auditoria NOT NULL, + fecha_programada DATE NOT NULL, + fecha_realizada DATE, + auditor VARCHAR(200), + resultado hse.resultado_auditoria, + no_conformidades INTEGER DEFAULT 0, + observaciones TEXT, + informe_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- RF-MAA017-006: GESTION AMBIENTAL +-- ============================================================================ + +-- Tabla: Catalogo de residuos +CREATE TABLE hse.residuos_catalogo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + codigo VARCHAR(20) NOT NULL UNIQUE, + nombre VARCHAR(200) NOT NULL, + categoria hse.categoria_residuo NOT NULL, + caracteristicas_cretib VARCHAR(6), + norma_referencia VARCHAR(50), + manejo_requerido TEXT, + tiempo_max_almacen_dias INTEGER, + requiere_manifiesto BOOLEAN NOT NULL DEFAULT false, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Generacion de residuos +CREATE TABLE hse.residuos_generacion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + residuo_id UUID NOT NULL REFERENCES hse.residuos_catalogo(id), + fecha_generacion DATE NOT NULL, + cantidad DECIMAL(10,2) NOT NULL, + unidad hse.unidad_residuo NOT NULL, + area_generacion VARCHAR(200), + fuente VARCHAR(200), + contenedor_id VARCHAR(50), + foto_url VARCHAR(500), + ubicacion_geo GEOMETRY(Point, 4326), + estado hse.estado_residuo NOT NULL DEFAULT 'almacenado', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: Almacenes temporales +CREATE TABLE hse.almacen_temporal ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + nombre VARCHAR(100) NOT NULL, + ubicacion VARCHAR(200), + capacidad_m3 DECIMAL(8,2), + tiene_contencion BOOLEAN NOT NULL DEFAULT true, + tiene_techo BOOLEAN NOT NULL DEFAULT true, + senalizacion_ok BOOLEAN NOT NULL DEFAULT true, + estado hse.estado_almacen NOT NULL DEFAULT 'operativo', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Proveedores ambientales +CREATE TABLE hse.proveedores_ambientales ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + razon_social VARCHAR(300) NOT NULL, + rfc VARCHAR(13), + tipo hse.tipo_proveedor_ambiental NOT NULL, + numero_autorizacion VARCHAR(50), + entidad_autorizadora VARCHAR(100), + fecha_autorizacion DATE, + fecha_vencimiento DATE, + servicios TEXT, + contacto_nombre VARCHAR(200), + contacto_telefono VARCHAR(20), + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Manifiestos de residuos +CREATE TABLE hse.manifiestos_residuos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + folio VARCHAR(30) NOT NULL, + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + transportista_id UUID NOT NULL REFERENCES hse.proveedores_ambientales(id), + destino_id UUID NOT NULL REFERENCES hse.proveedores_ambientales(id), + fecha_recoleccion DATE NOT NULL, + fecha_entrega DATE, + estado hse.estado_manifiesto NOT NULL DEFAULT 'emitido', + observaciones TEXT, + documento_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_manifiestos_residuos_folio UNIQUE (tenant_id, folio) +); + +-- Tabla: Detalle de manifiestos +CREATE TABLE hse.manifiesto_detalle ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + manifiesto_id UUID NOT NULL REFERENCES hse.manifiestos_residuos(id) ON DELETE CASCADE, + residuo_id UUID NOT NULL REFERENCES hse.residuos_catalogo(id), + generacion_ids UUID[], + cantidad DECIMAL(10,2) NOT NULL, + unidad hse.unidad_residuo NOT NULL, + descripcion TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Impacto ambiental +CREATE TABLE hse.impacto_ambiental ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + aspecto VARCHAR(200) NOT NULL, + tipo_impacto hse.tipo_impacto NOT NULL, + severidad hse.severidad NOT NULL, + probabilidad hse.probabilidad NOT NULL, + nivel_riesgo hse.nivel_riesgo NOT NULL, + medidas_mitigacion TEXT, + responsable_id UUID REFERENCES hr.employees(id), + estado hse.estado_impacto NOT NULL DEFAULT 'identificado', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Quejas ambientales +CREATE TABLE hse.quejas_ambientales ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + fecha_queja TIMESTAMPTZ NOT NULL, + origen hse.origen_queja NOT NULL, + tipo hse.tipo_queja NOT NULL, + descripcion TEXT NOT NULL, + nombre_quejoso VARCHAR(200), + contacto_quejoso VARCHAR(100), + acciones_tomadas TEXT, + estado hse.estado_queja NOT NULL DEFAULT 'recibida', + fecha_cierre DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- RF-MAA017-007: PERMISOS DE TRABAJO +-- ============================================================================ + +-- Tabla: Tipos de permiso de trabajo +CREATE TABLE hse.tipos_permiso_trabajo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + descripcion TEXT, + norma_referencia VARCHAR(50), + vigencia_max_horas INTEGER NOT NULL, + requiere_autorizacion_nivel INTEGER NOT NULL DEFAULT 2, + documentos_requeridos JSONB, + requisitos_personal JSONB, + equipos_requeridos JSONB, + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_tipos_permiso_trabajo_codigo UNIQUE (tenant_id, codigo) +); + +-- Tabla: Permisos de trabajo +CREATE TABLE hse.permisos_trabajo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + folio VARCHAR(30) NOT NULL, + tipo_permiso_id UUID NOT NULL REFERENCES hse.tipos_permiso_trabajo(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + solicitante_id UUID NOT NULL REFERENCES hr.employees(id), + descripcion_trabajo TEXT NOT NULL, + ubicacion VARCHAR(300) NOT NULL, + ubicacion_geo GEOMETRY(Point, 4326), + fecha_inicio_programada TIMESTAMPTZ NOT NULL, + fecha_fin_programada TIMESTAMPTZ NOT NULL, + fecha_inicio_real TIMESTAMPTZ, + fecha_fin_real TIMESTAMPTZ, + estado hse.estado_permiso NOT NULL DEFAULT 'borrador', + motivo_rechazo TEXT, + motivo_suspension TEXT, + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_permisos_trabajo_folio UNIQUE (tenant_id, folio) +); + +-- Tabla: Personal del permiso +CREATE TABLE hse.permiso_personal ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + employee_id UUID NOT NULL REFERENCES hr.employees(id), + rol hse.rol_permiso NOT NULL, + verificacion_capacitacion BOOLEAN NOT NULL DEFAULT false, + verificacion_epp BOOLEAN NOT NULL DEFAULT false, + verificacion_aptitud BOOLEAN NOT NULL DEFAULT false, + observaciones TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Autorizaciones de permiso +CREATE TABLE hse.permiso_autorizaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + nivel INTEGER NOT NULL, + autorizador_id UUID NOT NULL REFERENCES hr.employees(id), + rol_autorizador VARCHAR(100), + decision hse.decision_autorizacion NOT NULL, + observaciones TEXT, + firma_digital TEXT, + fecha_decision TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Checklist del permiso +CREATE TABLE hse.permiso_checklist ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + momento hse.momento_checklist NOT NULL, + item_verificacion VARCHAR(300) NOT NULL, + cumple BOOLEAN, + observacion TEXT, + verificador_id UUID REFERENCES hr.employees(id), + fecha_verificacion TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Monitoreos durante permiso +CREATE TABLE hse.permiso_monitoreos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + fecha_hora TIMESTAMPTZ NOT NULL, + tipo VARCHAR(100) NOT NULL, + valor_medicion VARCHAR(50), + unidad VARCHAR(20), + dentro_rango BOOLEAN NOT NULL DEFAULT true, + observaciones TEXT, + responsable_id UUID NOT NULL REFERENCES hr.employees(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Eventos del permiso +CREATE TABLE hse.permiso_eventos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + fecha_hora TIMESTAMPTZ NOT NULL DEFAULT NOW(), + tipo_evento hse.tipo_evento_permiso NOT NULL, + descripcion TEXT, + accion_tomada TEXT, + responsable_id UUID NOT NULL REFERENCES hr.employees(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Documentos del permiso +CREATE TABLE hse.permiso_documentos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, + tipo_documento VARCHAR(100) NOT NULL, + nombre VARCHAR(200) NOT NULL, + archivo_url VARCHAR(500) NOT NULL, + fecha_subida TIMESTAMPTZ NOT NULL DEFAULT NOW(), + subido_por UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- RF-MAA017-008: INDICADORES HSE +-- ============================================================================ + +-- Tabla: Configuracion de indicadores +CREATE TABLE hse.indicadores_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + descripcion TEXT, + formula TEXT, + unidad VARCHAR(50), + tipo hse.tipo_indicador NOT NULL, + meta_global DECIMAL(10,4), + umbral_verde DECIMAL(10,4), + umbral_amarillo DECIMAL(10,4), + umbral_rojo DECIMAL(10,4), + frecuencia_calculo hse.frecuencia_calculo NOT NULL DEFAULT 'mensual', + activo BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_indicadores_config_codigo UNIQUE (tenant_id, codigo) +); + +-- Tabla: Metas por obra +CREATE TABLE hse.indicadores_meta_obra ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + anio INTEGER NOT NULL, + meta DECIMAL(10,4) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Valores calculados de indicadores +CREATE TABLE hse.indicadores_valores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + periodo_tipo hse.periodo_tipo NOT NULL, + periodo_fecha DATE NOT NULL, + valor DECIMAL(15,4), + numerador DECIMAL(15,4), + denominador DECIMAL(15,4), + estado hse.estado_semaforo, + calculado_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Horas trabajadas +CREATE TABLE hse.horas_trabajadas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + fecha DATE NOT NULL, + horas_totales DECIMAL(12,2) NOT NULL, + trabajadores_promedio INTEGER NOT NULL, + fuente hse.fuente_horas NOT NULL DEFAULT 'manual', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_horas_trabajadas UNIQUE (tenant_id, fraccionamiento_id, fecha) +); + +-- Tabla: Dias sin accidente +CREATE TABLE hse.dias_sin_accidente ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + fecha_inicio_conteo DATE NOT NULL, + dias_acumulados INTEGER NOT NULL DEFAULT 0, + record_historico INTEGER NOT NULL DEFAULT 0, + ultimo_incidente_id UUID REFERENCES hse.incidentes(id), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT uq_dias_sin_accidente UNIQUE (tenant_id, fraccionamiento_id) +); + +-- Tabla: Reportes programados +CREATE TABLE hse.reportes_programados ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + nombre VARCHAR(200) NOT NULL, + tipo_reporte hse.tipo_reporte_hse NOT NULL, + indicadores UUID[], + fraccionamientos UUID[], + destinatarios VARCHAR[], + dia_envio INTEGER, + hora_envio TIME, + formato hse.formato_reporte NOT NULL DEFAULT 'pdf', + activo BOOLEAN NOT NULL DEFAULT true, + ultimo_envio TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla: Alertas de indicadores +CREATE TABLE hse.alertas_indicadores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id), + indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + tipo_alerta hse.tipo_alerta_indicador NOT NULL, + mensaje TEXT NOT NULL, + valor_actual DECIMAL(10,4), + valor_meta DECIMAL(10,4), + leida BOOLEAN NOT NULL DEFAULT false, + fecha_alerta TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +-- Incidentes +CREATE INDEX idx_incidentes_tenant ON hse.incidentes(tenant_id); +CREATE INDEX idx_incidentes_fraccionamiento ON hse.incidentes(fraccionamiento_id); +CREATE INDEX idx_incidentes_fecha ON hse.incidentes(fecha_hora); +CREATE INDEX idx_incidentes_estado ON hse.incidentes(estado); +CREATE INDEX idx_incidentes_tipo ON hse.incidentes(tipo); +CREATE INDEX idx_incidentes_gravedad ON hse.incidentes(gravedad); +CREATE INDEX idx_incidente_involucrados_employee ON hse.incidente_involucrados(employee_id); + +-- Capacitaciones +CREATE INDEX idx_capacitaciones_tenant ON hse.capacitaciones(tenant_id); +CREATE INDEX idx_capacitacion_sesiones_fecha ON hse.capacitacion_sesiones(fecha_programada); +CREATE INDEX idx_capacitacion_asistentes_employee ON hse.capacitacion_asistentes(employee_id); +CREATE INDEX idx_constancias_dc3_employee ON hse.constancias_dc3(employee_id); +CREATE INDEX idx_constancias_dc3_vencimiento ON hse.constancias_dc3(fecha_vencimiento); + +-- Inspecciones +CREATE INDEX idx_inspecciones_tenant ON hse.inspecciones(tenant_id); +CREATE INDEX idx_inspecciones_fraccionamiento ON hse.inspecciones(fraccionamiento_id); +CREATE INDEX idx_inspecciones_fecha ON hse.inspecciones(fecha_inicio); +CREATE INDEX idx_programa_inspecciones_fecha ON hse.programa_inspecciones(fecha_programada); +CREATE INDEX idx_hallazgos_estado ON hse.hallazgos(estado); +CREATE INDEX idx_hallazgos_fecha_limite ON hse.hallazgos(fecha_limite); + +-- EPP +CREATE INDEX idx_epp_asignaciones_employee ON hse.epp_asignaciones(employee_id); +CREATE INDEX idx_epp_asignaciones_vencimiento ON hse.epp_asignaciones(fecha_vencimiento); +CREATE INDEX idx_epp_asignaciones_estado ON hse.epp_asignaciones(estado); + +-- STPS +CREATE INDEX idx_comision_seguridad_vigencia ON hse.comision_seguridad(vigencia_fin); +CREATE INDEX idx_documentos_stps_vencimiento ON hse.documentos_stps(fecha_vencimiento); + +-- Ambiental +CREATE INDEX idx_residuos_generacion_fecha ON hse.residuos_generacion(fecha_generacion); +CREATE INDEX idx_manifiestos_estado ON hse.manifiestos_residuos(estado); + +-- Permisos +CREATE INDEX idx_permisos_trabajo_estado ON hse.permisos_trabajo(estado); +CREATE INDEX idx_permisos_trabajo_fecha ON hse.permisos_trabajo(fecha_inicio_programada); + +-- Indicadores +CREATE INDEX idx_indicadores_valores_fecha ON hse.indicadores_valores(periodo_fecha); +CREATE INDEX idx_horas_trabajadas_fecha ON hse.horas_trabajadas(fecha); + +-- ============================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================================ + +-- Habilitar RLS en todas las tablas con tenant_id +ALTER TABLE hse.incidentes ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.capacitaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.capacitacion_sesiones ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.instructores ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.constancias_dc3 ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.tipos_inspeccion ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.programa_inspecciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.inspecciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.hallazgos ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.epp_catalogo ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.epp_asignaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.epp_inventario ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.epp_movimientos ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.cumplimiento_obra ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.comision_seguridad ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.programa_seguridad ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.documentos_stps ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.auditorias ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.residuos_generacion ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.almacen_temporal ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.proveedores_ambientales ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.manifiestos_residuos ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.impacto_ambiental ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.quejas_ambientales ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.tipos_permiso_trabajo ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.permisos_trabajo ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.indicadores_config ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.indicadores_valores ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.horas_trabajadas ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.dias_sin_accidente ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.reportes_programados ENABLE ROW LEVEL SECURITY; +ALTER TABLE hse.alertas_indicadores ENABLE ROW LEVEL SECURITY; + +-- Politicas RLS (ejemplo para incidentes, replicar para las demas) +CREATE POLICY tenant_isolation_incidentes ON hse.incidentes + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_capacitaciones ON hse.capacitaciones + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_inspecciones ON hse.inspecciones + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_hallazgos ON hse.hallazgos + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_epp_asignaciones ON hse.epp_asignaciones + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_permisos_trabajo ON hse.permisos_trabajo + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_indicadores_valores ON hse.indicadores_valores + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- TRIGGERS PARA UPDATED_AT +-- ============================================================================ + +CREATE OR REPLACE FUNCTION hse.update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Aplicar trigger a tablas con updated_at +CREATE TRIGGER trg_incidentes_updated_at + BEFORE UPDATE ON hse.incidentes + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +CREATE TRIGGER trg_capacitaciones_updated_at + BEFORE UPDATE ON hse.capacitaciones + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +CREATE TRIGGER trg_inspecciones_updated_at + BEFORE UPDATE ON hse.inspecciones + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +CREATE TRIGGER trg_hallazgos_updated_at + BEFORE UPDATE ON hse.hallazgos + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +CREATE TRIGGER trg_epp_asignaciones_updated_at + BEFORE UPDATE ON hse.epp_asignaciones + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +CREATE TRIGGER trg_permisos_trabajo_updated_at + BEFORE UPDATE ON hse.permisos_trabajo + FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); + +-- ============================================================================ +-- DATOS INICIALES - CATALOGO DE NORMAS STPS +-- ============================================================================ + +INSERT INTO hse.normas_stps (codigo, nombre, descripcion, aplica_construccion) VALUES +('NOM-001-STPS-2008', 'Edificios, locales e instalaciones', 'Condiciones de seguridad', true), +('NOM-002-STPS-2010', 'Prevencion y proteccion contra incendios', 'Prevencion y proteccion contra incendios en centros de trabajo', true), +('NOM-004-STPS-1999', 'Sistemas de proteccion y dispositivos de seguridad en maquinaria', 'Sistemas de proteccion en maquinaria y equipo', true), +('NOM-005-STPS-1998', 'Manejo de sustancias quimicas peligrosas', 'Relativa a condiciones de seguridad e higiene', true), +('NOM-006-STPS-2014', 'Manejo y almacenamiento de materiales', 'Condiciones de seguridad y salud en el trabajo', true), +('NOM-009-STPS-2011', 'Trabajos en altura', 'Condiciones de seguridad para realizar trabajos en altura', true), +('NOM-011-STPS-2001', 'Ruido', 'Condiciones de seguridad e higiene donde se genere ruido', true), +('NOM-017-STPS-2008', 'Equipo de proteccion personal', 'Seleccion, uso y manejo en centros de trabajo', true), +('NOM-019-STPS-2011', 'Comisiones de seguridad e higiene', 'Constitucion, integracion, organizacion y funcionamiento', true), +('NOM-026-STPS-2008', 'Senales de seguridad', 'Colores y senales de seguridad e higiene', true), +('NOM-029-STPS-2011', 'Mantenimiento de instalaciones electricas', 'Condiciones de seguridad para realizar actividades', true), +('NOM-030-STPS-2009', 'Servicios preventivos de seguridad y salud', 'Funciones y actividades', true), +('NOM-031-STPS-2011', 'Construccion', 'Condiciones de seguridad y salud en el trabajo', true); + +-- ============================================================================ +-- DATOS INICIALES - CATALOGO DE RESIDUOS COMUNES EN CONSTRUCCION +-- ============================================================================ + +INSERT INTO hse.residuos_catalogo (codigo, nombre, categoria, caracteristicas_cretib, norma_referencia, tiempo_max_almacen_dias, requiere_manifiesto) VALUES +('RP-001', 'Aceites usados', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), +('RP-002', 'Solventes usados', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), +('RP-003', 'Pinturas y barnices', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), +('RP-004', 'Baterias y acumuladores', 'peligroso', 'CT', 'NOM-052-SEMARNAT', 180, true), +('RP-005', 'Envases contaminados', 'peligroso', 'T', 'NOM-052-SEMARNAT', 180, true), +('RP-006', 'Trapos y estopas impregnados', 'peligroso', 'I', 'NOM-052-SEMARNAT', 180, true), +('RME-001', 'Escombro y cascajo', 'manejo_especial', NULL, 'LGPGIR', 365, false), +('RME-002', 'Tierra excavada', 'manejo_especial', NULL, 'LGPGIR', 365, false), +('RME-003', 'Material de demolicion', 'manejo_especial', NULL, 'LGPGIR', 365, false), +('RSU-001', 'Carton y papel', 'urbano', NULL, NULL, NULL, false), +('RSU-002', 'Plasticos', 'urbano', NULL, NULL, NULL, false), +('RSU-003', 'Residuos organicos', 'urbano', NULL, NULL, NULL, false); + +-- ============================================================================ +-- COMENTARIOS DE DOCUMENTACION +-- ============================================================================ + +COMMENT ON SCHEMA hse IS 'Schema para gestion de Seguridad, Salud Ocupacional y Medio Ambiente (HSE) - MAA-017'; +COMMENT ON TABLE hse.incidentes IS 'Registro de incidentes y accidentes de trabajo'; +COMMENT ON TABLE hse.capacitaciones IS 'Catalogo de capacitaciones de seguridad'; +COMMENT ON TABLE hse.inspecciones IS 'Registro de inspecciones de seguridad ejecutadas'; +COMMENT ON TABLE hse.hallazgos IS 'Hallazgos detectados en inspecciones'; +COMMENT ON TABLE hse.epp_asignaciones IS 'Asignacion de EPP a trabajadores'; +COMMENT ON TABLE hse.permisos_trabajo IS 'Permisos para trabajos de alto riesgo'; +COMMENT ON TABLE hse.indicadores_valores IS 'Valores calculados de indicadores HSE'; + +-- ============================================================================ +-- FIN DEL DDL +-- ============================================================================ diff --git a/schemas/04-estimates-schema-ddl.sql b/schemas/04-estimates-schema-ddl.sql new file mode 100644 index 0000000..c89c116 --- /dev/null +++ b/schemas/04-estimates-schema-ddl.sql @@ -0,0 +1,415 @@ +-- ============================================================================ +-- ESTIMATES Schema DDL - Estimaciones, Anticipos y Retenciones +-- Modulos: MAI-008 (Estimaciones y Facturación) +-- Version: 1.0.0 +-- Fecha: 2025-12-08 +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth.tenants, auth.users) +-- 2. Schema construction instalado (fraccionamientos, contratos, conceptos, lotes, departamentos) +-- ============================================================================ + +-- Verificar prerequisitos +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_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; + END IF; +END $$; + +-- Crear schema +CREATE SCHEMA IF NOT EXISTS estimates; + +-- ============================================================================ +-- TYPES (ENUMs) +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE estimates.estimate_status AS ENUM ( + 'draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE estimates.advance_type AS ENUM ( + 'initial', 'progress', 'materials' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE estimates.retention_type AS ENUM ( + 'guarantee', 'tax', 'penalty', 'other' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE estimates.generator_status AS ENUM ( + 'draft', 'in_progress', 'completed', 'approved' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================ +-- TABLES - ESTIMACIONES +-- ============================================================================ + +-- Tabla: estimaciones (estimaciones de obra) +CREATE TABLE IF NOT EXISTS estimates.estimaciones ( + 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), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + estimate_number VARCHAR(30) NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + sequence_number INTEGER NOT NULL, + status estimates.estimate_status NOT NULL DEFAULT 'draft', + subtotal DECIMAL(16,2) DEFAULT 0, + advance_amount DECIMAL(16,2) DEFAULT 0, + retention_amount DECIMAL(16,2) DEFAULT 0, + tax_amount DECIMAL(16,2) DEFAULT 0, + total_amount DECIMAL(16,2) DEFAULT 0, + submitted_at TIMESTAMPTZ, + submitted_by UUID REFERENCES auth.users(id), + reviewed_at TIMESTAMPTZ, + reviewed_by UUID REFERENCES auth.users(id), + approved_at TIMESTAMPTZ, + approved_by UUID REFERENCES auth.users(id), + invoice_id UUID, + invoiced_at TIMESTAMPTZ, + paid_at TIMESTAMPTZ, + notes TEXT, + 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_estimaciones_number_tenant UNIQUE (tenant_id, estimate_number), + CONSTRAINT uq_estimaciones_sequence_contrato UNIQUE (contrato_id, sequence_number), + CONSTRAINT chk_estimaciones_period CHECK (period_end >= period_start) +); + +-- Tabla: estimacion_conceptos (líneas de estimación) +CREATE TABLE IF NOT EXISTS estimates.estimacion_conceptos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE, + concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), + contrato_partida_id UUID REFERENCES construction.contrato_partidas(id), + quantity_contract DECIMAL(12,4) DEFAULT 0, + quantity_previous DECIMAL(12,4) DEFAULT 0, + quantity_current DECIMAL(12,4) DEFAULT 0, + quantity_accumulated DECIMAL(12,4) GENERATED ALWAYS AS (quantity_previous + quantity_current) STORED, + unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, + amount_current DECIMAL(14,2) GENERATED ALWAYS AS (quantity_current * unit_price) STORED, + notes TEXT, + 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_est_conceptos_estimacion_concepto UNIQUE (estimacion_id, concepto_id) +); + +-- Tabla: generadores (soporte de cantidades) +CREATE TABLE IF NOT EXISTS estimates.generadores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + estimacion_concepto_id UUID NOT NULL REFERENCES estimates.estimacion_conceptos(id) ON DELETE CASCADE, + generator_number VARCHAR(30) NOT NULL, + description TEXT, + status estimates.generator_status NOT NULL DEFAULT 'draft', + lote_id UUID REFERENCES construction.lotes(id), + departamento_id UUID REFERENCES construction.departamentos(id), + location_description VARCHAR(255), + quantity DECIMAL(12,4) NOT NULL DEFAULT 0, + formula TEXT, + photo_url VARCHAR(500), + sketch_url VARCHAR(500), + captured_by UUID NOT NULL REFERENCES auth.users(id), + captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + 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) +); + +-- ============================================================================ +-- TABLES - ANTICIPOS +-- ============================================================================ + +-- Tabla: anticipos (anticipos otorgados) +CREATE TABLE IF NOT EXISTS estimates.anticipos ( + 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), + advance_type estimates.advance_type NOT NULL DEFAULT 'initial', + advance_number VARCHAR(30) NOT NULL, + advance_date DATE NOT NULL, + gross_amount DECIMAL(16,2) NOT NULL, + tax_amount DECIMAL(16,2) DEFAULT 0, + net_amount DECIMAL(16,2) NOT NULL, + amortization_percentage DECIMAL(5,2) DEFAULT 0, + amortized_amount DECIMAL(16,2) DEFAULT 0, + pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (net_amount - amortized_amount) STORED, + is_fully_amortized BOOLEAN DEFAULT FALSE, + approved_at TIMESTAMPTZ, + approved_by UUID REFERENCES auth.users(id), + paid_at TIMESTAMPTZ, + payment_reference VARCHAR(100), + notes TEXT, + 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_anticipos_number_tenant UNIQUE (tenant_id, advance_number) +); + +-- Tabla: amortizaciones (amortizaciones de anticipos) +CREATE TABLE IF NOT EXISTS estimates.amortizaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + anticipo_id UUID NOT NULL REFERENCES estimates.anticipos(id), + estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id), + amount DECIMAL(16,2) NOT NULL, + amortization_date DATE NOT NULL, + notes TEXT, + 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_amortizaciones_anticipo_estimacion UNIQUE (anticipo_id, estimacion_id) +); + +-- ============================================================================ +-- TABLES - RETENCIONES +-- ============================================================================ + +-- Tabla: retenciones (retenciones aplicadas) +CREATE TABLE IF NOT EXISTS estimates.retenciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id), + retention_type estimates.retention_type NOT NULL, + description VARCHAR(255) NOT NULL, + percentage DECIMAL(5,2), + amount DECIMAL(16,2) NOT NULL, + release_date DATE, + released_at TIMESTAMPTZ, + released_amount DECIMAL(16,2), + notes TEXT, + 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: fondo_garantia (acumulado de fondo de garantía) +CREATE TABLE IF NOT EXISTS estimates.fondo_garantia ( + 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), + accumulated_amount DECIMAL(16,2) DEFAULT 0, + released_amount DECIMAL(16,2) DEFAULT 0, + pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (accumulated_amount - released_amount) STORED, + release_date DATE, + released_at TIMESTAMPTZ, + released_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_fondo_garantia_contrato UNIQUE (contrato_id) +); + +-- ============================================================================ +-- TABLES - WORKFLOW +-- ============================================================================ + +-- Tabla: estimacion_workflow (historial de workflow) +CREATE TABLE IF NOT EXISTS estimates.estimacion_workflow ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE, + from_status estimates.estimate_status, + to_status estimates.estimate_status NOT NULL, + action VARCHAR(50) NOT NULL, + comments TEXT, + performed_by UUID NOT NULL REFERENCES auth.users(id), + performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_estimaciones_tenant_id ON estimates.estimaciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_estimaciones_contrato_id ON estimates.estimaciones(contrato_id); +CREATE INDEX IF NOT EXISTS idx_estimaciones_fraccionamiento_id ON estimates.estimaciones(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_estimaciones_status ON estimates.estimaciones(status); +CREATE INDEX IF NOT EXISTS idx_estimaciones_period ON estimates.estimaciones(period_start, period_end); + +CREATE INDEX IF NOT EXISTS idx_est_conceptos_tenant_id ON estimates.estimacion_conceptos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_est_conceptos_estimacion_id ON estimates.estimacion_conceptos(estimacion_id); +CREATE INDEX IF NOT EXISTS idx_est_conceptos_concepto_id ON estimates.estimacion_conceptos(concepto_id); + +CREATE INDEX IF NOT EXISTS idx_generadores_tenant_id ON estimates.generadores(tenant_id); +CREATE INDEX IF NOT EXISTS idx_generadores_est_concepto_id ON estimates.generadores(estimacion_concepto_id); +CREATE INDEX IF NOT EXISTS idx_generadores_status ON estimates.generadores(status); + +CREATE INDEX IF NOT EXISTS idx_anticipos_tenant_id ON estimates.anticipos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_anticipos_contrato_id ON estimates.anticipos(contrato_id); +CREATE INDEX IF NOT EXISTS idx_anticipos_type ON estimates.anticipos(advance_type); + +CREATE INDEX IF NOT EXISTS idx_amortizaciones_tenant_id ON estimates.amortizaciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_amortizaciones_anticipo_id ON estimates.amortizaciones(anticipo_id); +CREATE INDEX IF NOT EXISTS idx_amortizaciones_estimacion_id ON estimates.amortizaciones(estimacion_id); + +CREATE INDEX IF NOT EXISTS idx_retenciones_tenant_id ON estimates.retenciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_retenciones_estimacion_id ON estimates.retenciones(estimacion_id); +CREATE INDEX IF NOT EXISTS idx_retenciones_type ON estimates.retenciones(retention_type); + +CREATE INDEX IF NOT EXISTS idx_fondo_garantia_tenant_id ON estimates.fondo_garantia(tenant_id); +CREATE INDEX IF NOT EXISTS idx_fondo_garantia_contrato_id ON estimates.fondo_garantia(contrato_id); + +CREATE INDEX IF NOT EXISTS idx_est_workflow_tenant_id ON estimates.estimacion_workflow(tenant_id); +CREATE INDEX IF NOT EXISTS idx_est_workflow_estimacion_id ON estimates.estimacion_workflow(estimacion_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================================ + +ALTER TABLE estimates.estimaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.estimacion_conceptos ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.generadores ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.anticipos ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.amortizaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.retenciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.fondo_garantia ENABLE ROW LEVEL SECURITY; +ALTER TABLE estimates.estimacion_workflow ENABLE ROW LEVEL SECURITY; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_estimaciones ON estimates.estimaciones; + CREATE POLICY tenant_isolation_estimaciones ON estimates.estimaciones + 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_est_conceptos ON estimates.estimacion_conceptos; + CREATE POLICY tenant_isolation_est_conceptos ON estimates.estimacion_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_generadores ON estimates.generadores; + CREATE POLICY tenant_isolation_generadores ON estimates.generadores + 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_anticipos ON estimates.anticipos; + CREATE POLICY tenant_isolation_anticipos ON estimates.anticipos + 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_amortizaciones ON estimates.amortizaciones; + CREATE POLICY tenant_isolation_amortizaciones ON estimates.amortizaciones + 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_retenciones ON estimates.retenciones; + CREATE POLICY tenant_isolation_retenciones ON estimates.retenciones + 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_fondo_garantia ON estimates.fondo_garantia; + CREATE POLICY tenant_isolation_fondo_garantia ON estimates.fondo_garantia + 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_est_workflow ON estimates.estimacion_workflow; + CREATE POLICY tenant_isolation_est_workflow ON estimates.estimacion_workflow + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- FUNCIONES +-- ============================================================================ + +-- Función: calcular totales de estimación +CREATE OR REPLACE FUNCTION estimates.calculate_estimate_totals(p_estimacion_id UUID) +RETURNS VOID AS $$ +DECLARE + v_subtotal DECIMAL(16,2); + v_advance DECIMAL(16,2); + v_retention DECIMAL(16,2); + v_tax_rate DECIMAL(5,2) := 0.16; + v_tax DECIMAL(16,2); + v_total DECIMAL(16,2); +BEGIN + SELECT COALESCE(SUM(amount_current), 0) INTO v_subtotal + FROM estimates.estimacion_conceptos + WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; + + SELECT COALESCE(SUM(amount), 0) INTO v_advance + FROM estimates.amortizaciones + WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; + + SELECT COALESCE(SUM(amount), 0) INTO v_retention + FROM estimates.retenciones + WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; + + v_tax := v_subtotal * v_tax_rate; + v_total := v_subtotal + v_tax - v_advance - v_retention; + + UPDATE estimates.estimaciones + SET subtotal = v_subtotal, + advance_amount = v_advance, + retention_amount = v_retention, + tax_amount = v_tax, + total_amount = v_total, + updated_at = NOW() + WHERE id = p_estimacion_id; +END; +$$ LANGUAGE plpgsql; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON SCHEMA estimates IS 'Schema de estimaciones, anticipos y retenciones de obra'; +COMMENT ON TABLE estimates.estimaciones IS 'Estimaciones de obra periódicas'; +COMMENT ON TABLE estimates.estimacion_conceptos IS 'Líneas de concepto por estimación'; +COMMENT ON TABLE estimates.generadores IS 'Generadores de cantidades para estimaciones'; +COMMENT ON TABLE estimates.anticipos IS 'Anticipos otorgados a subcontratistas'; +COMMENT ON TABLE estimates.amortizaciones IS 'Amortizaciones de anticipos por estimación'; +COMMENT ON TABLE estimates.retenciones IS 'Retenciones aplicadas a estimaciones'; +COMMENT ON TABLE estimates.fondo_garantia IS 'Fondo de garantía acumulado por contrato'; +COMMENT ON TABLE estimates.estimacion_workflow IS 'Historial de workflow de estimaciones'; + +-- ============================================================================ +-- FIN DEL SCHEMA ESTIMATES +-- Total tablas: 8 +-- ============================================================================ diff --git a/schemas/05-infonavit-schema-ddl.sql b/schemas/05-infonavit-schema-ddl.sql new file mode 100644 index 0000000..00676d8 --- /dev/null +++ b/schemas/05-infonavit-schema-ddl.sql @@ -0,0 +1,413 @@ +-- ============================================================================ +-- INFONAVIT Schema DDL - Cumplimiento INFONAVIT y Derechohabientes +-- Modulos: MAI-010 (CRM Derechohabientes), MAI-011 (Integración INFONAVIT) +-- Version: 1.0.0 +-- Fecha: 2025-12-08 +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth.tenants, auth.users, auth.companies) +-- 2. Schema construction instalado (fraccionamientos, lotes, departamentos) +-- ============================================================================ + +-- Verificar prerequisitos +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_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; + END IF; +END $$; + +-- Crear schema +CREATE SCHEMA IF NOT EXISTS infonavit; + +-- ============================================================================ +-- TYPES (ENUMs) +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE infonavit.derechohabiente_status AS ENUM ( + 'prospect', 'pre_qualified', 'qualified', 'assigned', 'in_process', 'owner', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE infonavit.credit_type AS ENUM ( + 'infonavit_tradicional', 'infonavit_total', 'cofinavit', 'mejoravit', + 'fovissste', 'fovissste_infonavit', 'bank_credit', 'cash' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE infonavit.acta_type AS ENUM ( + 'inicio_obra', 'verificacion_avance', 'entrega_recepcion', 'conclusion_obra', 'liberacion_vivienda' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE infonavit.acta_status AS ENUM ( + 'draft', 'pending', 'signed', 'submitted', 'approved', 'rejected', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE infonavit.report_type AS ENUM ( + 'avance_fisico', 'avance_financiero', 'inventario_viviendas', 'asignaciones', 'escrituraciones', 'cartera_vencida' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================ +-- TABLES - REGISTRO INFONAVIT +-- ============================================================================ + +-- Tabla: registro_infonavit (registro del constructor ante INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.registro_infonavit ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + company_id UUID NOT NULL, + registro_number VARCHAR(50) NOT NULL, + registro_date DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + vigencia_start DATE, + vigencia_end DATE, + responsable_tecnico VARCHAR(255), + cedula_profesional VARCHAR(50), + 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_registro_infonavit_tenant UNIQUE (tenant_id, registro_number) +); + +-- Tabla: oferta_vivienda (oferta de viviendas ante INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.oferta_vivienda ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + registro_id UUID NOT NULL REFERENCES infonavit.registro_infonavit(id), + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + oferta_number VARCHAR(50) NOT NULL, + submission_date DATE NOT NULL, + approval_date DATE, + total_units INTEGER NOT NULL DEFAULT 0, + approved_units INTEGER DEFAULT 0, + price_range_min DECIMAL(14,2), + price_range_max DECIMAL(14,2), + status VARCHAR(20) NOT NULL DEFAULT 'pending', + rejection_reason TEXT, + 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_oferta_vivienda_tenant UNIQUE (tenant_id, oferta_number) +); + +-- ============================================================================ +-- TABLES - DERECHOHABIENTES +-- ============================================================================ + +-- Tabla: derechohabientes (compradores con crédito INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.derechohabientes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + partner_id UUID, + nss VARCHAR(15) NOT NULL, + curp VARCHAR(18), + rfc VARCHAR(13), + full_name VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + second_last_name VARCHAR(100), + birth_date DATE, + gender VARCHAR(10), + marital_status VARCHAR(20), + nationality VARCHAR(50) DEFAULT 'Mexicana', + email VARCHAR(255), + phone VARCHAR(20), + mobile VARCHAR(20), + address TEXT, + city VARCHAR(100), + state VARCHAR(100), + zip_code VARCHAR(10), + employer_name VARCHAR(255), + employer_rfc VARCHAR(13), + employment_start_date DATE, + salary DECIMAL(12,2), + cotization_weeks INTEGER, + credit_type infonavit.credit_type, + credit_number VARCHAR(50), + credit_amount DECIMAL(14,2), + puntos_infonavit DECIMAL(10,2), + subcuenta_vivienda DECIMAL(14,2), + precalificacion_date DATE, + precalificacion_amount DECIMAL(14,2), + status infonavit.derechohabiente_status NOT NULL DEFAULT 'prospect', + 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_derechohabientes_nss_tenant UNIQUE (tenant_id, nss) +); + +-- Tabla: asignacion_vivienda (asignación de vivienda a derechohabiente) +CREATE TABLE IF NOT EXISTS infonavit.asignacion_vivienda ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + derechohabiente_id UUID NOT NULL REFERENCES infonavit.derechohabientes(id), + lote_id UUID REFERENCES construction.lotes(id), + departamento_id UUID REFERENCES construction.departamentos(id), + oferta_id UUID REFERENCES infonavit.oferta_vivienda(id), + assignment_date DATE NOT NULL, + assignment_number VARCHAR(50), + status VARCHAR(20) NOT NULL DEFAULT 'pending', + sale_price DECIMAL(14,2) NOT NULL, + credit_amount DECIMAL(14,2), + down_payment DECIMAL(14,2), + subsidy_amount DECIMAL(14,2), + notary_name VARCHAR(255), + notary_number VARCHAR(50), + deed_date DATE, + deed_number VARCHAR(50), + public_registry_number VARCHAR(50), + public_registry_date DATE, + scheduled_delivery_date DATE, + actual_delivery_date DATE, + delivery_act_id UUID, + notes TEXT, + 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_asignacion_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) + ) +); + +-- ============================================================================ +-- TABLES - ACTAS +-- ============================================================================ + +-- Tabla: actas (actas INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.actas ( + 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), + acta_type infonavit.acta_type NOT NULL, + acta_number VARCHAR(50) NOT NULL, + acta_date DATE NOT NULL, + status infonavit.acta_status NOT NULL DEFAULT 'draft', + infonavit_representative VARCHAR(255), + constructor_representative VARCHAR(255), + perito_name VARCHAR(255), + perito_cedula VARCHAR(50), + description TEXT, + observations TEXT, + agreements TEXT, + physical_advance_percentage DECIMAL(5,2), + financial_advance_percentage DECIMAL(5,2), + signed_at TIMESTAMPTZ, + submitted_to_infonavit_at TIMESTAMPTZ, + infonavit_response_at TIMESTAMPTZ, + infonavit_folio VARCHAR(50), + document_url VARCHAR(500), + signed_document_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), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id), + CONSTRAINT uq_actas_number_tenant UNIQUE (tenant_id, acta_number) +); + +-- Tabla: acta_viviendas (viviendas incluidas en acta) +CREATE TABLE IF NOT EXISTS infonavit.acta_viviendas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + acta_id UUID NOT NULL REFERENCES infonavit.actas(id) ON DELETE CASCADE, + lote_id UUID REFERENCES construction.lotes(id), + departamento_id UUID REFERENCES construction.departamentos(id), + advance_percentage DECIMAL(5,2), + observations TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- TABLES - REPORTES INFONAVIT +-- ============================================================================ + +-- Tabla: reportes_infonavit (reportes enviados a INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.reportes_infonavit ( + 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), + report_type infonavit.report_type NOT NULL, + report_number VARCHAR(50) NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + submission_date DATE, + status VARCHAR(20) NOT NULL DEFAULT 'draft', + infonavit_folio VARCHAR(50), + total_units INTEGER, + units_in_progress INTEGER, + units_completed INTEGER, + units_delivered INTEGER, + physical_advance_percentage DECIMAL(5,2), + financial_advance_percentage DECIMAL(5,2), + document_url VARCHAR(500), + acknowledgment_url VARCHAR(500), + notes TEXT, + 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_reportes_number_tenant UNIQUE (tenant_id, report_number) +); + +-- Tabla: historico_puntos (histórico de puntos INFONAVIT) +CREATE TABLE IF NOT EXISTS infonavit.historico_puntos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + derechohabiente_id UUID NOT NULL REFERENCES infonavit.derechohabientes(id), + query_date DATE NOT NULL, + puntos DECIMAL(10,2), + subcuenta_vivienda DECIMAL(14,2), + cotization_weeks INTEGER, + credit_capacity DECIMAL(14,2), + source VARCHAR(50), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_registro_infonavit_tenant_id ON infonavit.registro_infonavit(tenant_id); +CREATE INDEX IF NOT EXISTS idx_registro_infonavit_company_id ON infonavit.registro_infonavit(company_id); + +CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_tenant_id ON infonavit.oferta_vivienda(tenant_id); +CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_registro_id ON infonavit.oferta_vivienda(registro_id); +CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_fraccionamiento_id ON infonavit.oferta_vivienda(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_status ON infonavit.oferta_vivienda(status); + +CREATE INDEX IF NOT EXISTS idx_derechohabientes_tenant_id ON infonavit.derechohabientes(tenant_id); +CREATE INDEX IF NOT EXISTS idx_derechohabientes_nss ON infonavit.derechohabientes(nss); +CREATE INDEX IF NOT EXISTS idx_derechohabientes_curp ON infonavit.derechohabientes(curp); +CREATE INDEX IF NOT EXISTS idx_derechohabientes_status ON infonavit.derechohabientes(status); +CREATE INDEX IF NOT EXISTS idx_derechohabientes_credit_type ON infonavit.derechohabientes(credit_type); + +CREATE INDEX IF NOT EXISTS idx_asignacion_tenant_id ON infonavit.asignacion_vivienda(tenant_id); +CREATE INDEX IF NOT EXISTS idx_asignacion_derechohabiente_id ON infonavit.asignacion_vivienda(derechohabiente_id); +CREATE INDEX IF NOT EXISTS idx_asignacion_lote_id ON infonavit.asignacion_vivienda(lote_id); +CREATE INDEX IF NOT EXISTS idx_asignacion_status ON infonavit.asignacion_vivienda(status); + +CREATE INDEX IF NOT EXISTS idx_actas_tenant_id ON infonavit.actas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_actas_fraccionamiento_id ON infonavit.actas(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_actas_type ON infonavit.actas(acta_type); +CREATE INDEX IF NOT EXISTS idx_actas_status ON infonavit.actas(status); + +CREATE INDEX IF NOT EXISTS idx_acta_viviendas_tenant_id ON infonavit.acta_viviendas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_acta_viviendas_acta_id ON infonavit.acta_viviendas(acta_id); + +CREATE INDEX IF NOT EXISTS idx_reportes_tenant_id ON infonavit.reportes_infonavit(tenant_id); +CREATE INDEX IF NOT EXISTS idx_reportes_fraccionamiento_id ON infonavit.reportes_infonavit(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_reportes_type ON infonavit.reportes_infonavit(report_type); + +CREATE INDEX IF NOT EXISTS idx_historico_puntos_tenant_id ON infonavit.historico_puntos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_historico_puntos_derechohabiente_id ON infonavit.historico_puntos(derechohabiente_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================================ + +ALTER TABLE infonavit.registro_infonavit ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.oferta_vivienda ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.derechohabientes ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.asignacion_vivienda ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.actas ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.acta_viviendas ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.reportes_infonavit ENABLE ROW LEVEL SECURITY; +ALTER TABLE infonavit.historico_puntos ENABLE ROW LEVEL SECURITY; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_registro_infonavit ON infonavit.registro_infonavit; + CREATE POLICY tenant_isolation_registro_infonavit ON infonavit.registro_infonavit + 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_oferta_vivienda ON infonavit.oferta_vivienda; + CREATE POLICY tenant_isolation_oferta_vivienda ON infonavit.oferta_vivienda + 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_derechohabientes ON infonavit.derechohabientes; + CREATE POLICY tenant_isolation_derechohabientes ON infonavit.derechohabientes + 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_asignacion_vivienda ON infonavit.asignacion_vivienda; + CREATE POLICY tenant_isolation_asignacion_vivienda ON infonavit.asignacion_vivienda + 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_actas ON infonavit.actas; + CREATE POLICY tenant_isolation_actas ON infonavit.actas + 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_acta_viviendas ON infonavit.acta_viviendas; + CREATE POLICY tenant_isolation_acta_viviendas ON infonavit.acta_viviendas + 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_reportes_infonavit ON infonavit.reportes_infonavit; + CREATE POLICY tenant_isolation_reportes_infonavit ON infonavit.reportes_infonavit + 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_historico_puntos ON infonavit.historico_puntos; + CREATE POLICY tenant_isolation_historico_puntos ON infonavit.historico_puntos + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON SCHEMA infonavit IS 'Schema de cumplimiento INFONAVIT y gestión de derechohabientes'; +COMMENT ON TABLE infonavit.registro_infonavit IS 'Registro del constructor ante INFONAVIT'; +COMMENT ON TABLE infonavit.oferta_vivienda IS 'Oferta de viviendas registrada ante INFONAVIT'; +COMMENT ON TABLE infonavit.derechohabientes IS 'Derechohabientes INFONAVIT/compradores'; +COMMENT ON TABLE infonavit.asignacion_vivienda IS 'Asignación de vivienda a derechohabiente'; +COMMENT ON TABLE infonavit.actas IS 'Actas oficiales INFONAVIT'; +COMMENT ON TABLE infonavit.acta_viviendas IS 'Viviendas incluidas en cada acta'; +COMMENT ON TABLE infonavit.reportes_infonavit IS 'Reportes periódicos enviados a INFONAVIT'; +COMMENT ON TABLE infonavit.historico_puntos IS 'Histórico de consulta de puntos INFONAVIT'; + +-- ============================================================================ +-- FIN DEL SCHEMA INFONAVIT +-- Total tablas: 8 +-- ============================================================================ diff --git a/schemas/06-inventory-ext-schema-ddl.sql b/schemas/06-inventory-ext-schema-ddl.sql new file mode 100644 index 0000000..ce643ab --- /dev/null +++ b/schemas/06-inventory-ext-schema-ddl.sql @@ -0,0 +1,213 @@ +-- ============================================================================ +-- INVENTORY EXTENSION Schema DDL - Extensiones de Inventario para Construcción +-- Modulos: MAI-004 (Compras e Inventarios) +-- Version: 1.0.0 +-- Fecha: 2025-12-08 +-- ============================================================================ +-- TIPO: Extensión del ERP Core (MGN-005 Inventory) +-- NOTA: Contiene SOLO extensiones específicas de construcción. +-- Las tablas base están en el ERP Core. +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth.tenants, auth.users) +-- 2. Schema construction instalado (fraccionamientos, conceptos, lotes, departamentos) +-- 3. Schema inventory de ERP-Core instalado (opcional, para FKs) +-- ============================================================================ + +-- Verificar prerequisitos +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_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; + END IF; +END $$; + +-- Crear schema si no existe (puede ya existir desde ERP-Core) +CREATE SCHEMA IF NOT EXISTS inventory; + +-- ============================================================================ +-- TYPES (ENUMs) ADICIONALES +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE inventory.warehouse_type_construction AS ENUM ( + 'central', 'obra', 'temporal', 'transito' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE inventory.requisition_status AS ENUM ( + 'draft', 'submitted', 'approved', 'partially_served', 'served', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================ +-- TABLES - EXTENSIONES CONSTRUCCIÓN +-- ============================================================================ + +-- Tabla: almacenes_proyecto (almacén por proyecto/obra) +-- Extiende: inventory.warehouses (ERP Core) +CREATE TABLE IF NOT EXISTS inventory.almacenes_proyecto ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + warehouse_id UUID NOT NULL, -- FK a inventory.warehouses (ERP Core) + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + warehouse_type inventory.warehouse_type_construction NOT NULL DEFAULT 'obra', + location_description TEXT, + location GEOMETRY(POINT, 4326), + responsible_id UUID REFERENCES auth.users(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_almacenes_proyecto_warehouse UNIQUE (warehouse_id) +); + +-- Tabla: requisiciones_obra (requisiciones desde obra) +CREATE TABLE IF NOT EXISTS inventory.requisiciones_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), + requisition_number VARCHAR(30) NOT NULL, + requisition_date DATE NOT NULL, + required_date DATE NOT NULL, + status inventory.requisition_status NOT NULL DEFAULT 'draft', + priority VARCHAR(20) DEFAULT 'medium', + requested_by UUID NOT NULL REFERENCES auth.users(id), + destination_warehouse_id UUID, -- FK a inventory.warehouses (ERP Core) + approved_by UUID REFERENCES auth.users(id), + approved_at TIMESTAMPTZ, + rejection_reason TEXT, + purchase_order_id UUID, -- FK a purchase.purchase_orders (ERP Core) + notes TEXT, + 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_requisiciones_obra_number UNIQUE (tenant_id, requisition_number) +); + +-- Tabla: requisicion_lineas (líneas de requisición) +CREATE TABLE IF NOT EXISTS inventory.requisicion_lineas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + requisicion_id UUID NOT NULL REFERENCES inventory.requisiciones_obra(id) ON DELETE CASCADE, + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + concepto_id UUID REFERENCES construction.conceptos(id), + lote_id UUID REFERENCES construction.lotes(id), + quantity_requested DECIMAL(12,4) NOT NULL, + quantity_approved DECIMAL(12,4), + quantity_served DECIMAL(12,4) DEFAULT 0, + unit_id UUID, -- FK a core.uom (ERP Core) + notes TEXT, + 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: consumos_obra (consumos de materiales por obra/lote) +CREATE TABLE IF NOT EXISTS inventory.consumos_obra ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + stock_move_id UUID, -- FK a inventory.stock_moves (ERP Core) + fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), + lote_id UUID REFERENCES construction.lotes(id), + departamento_id UUID REFERENCES construction.departamentos(id), + concepto_id UUID REFERENCES construction.conceptos(id), + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + quantity DECIMAL(12,4) NOT NULL, + unit_cost DECIMAL(12,4), + total_cost DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_cost) STORED, + consumption_date DATE NOT NULL, + registered_by UUID NOT NULL REFERENCES auth.users(id), + notes TEXT, + 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 +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_tenant_id ON inventory.almacenes_proyecto(tenant_id); +CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_warehouse_id ON inventory.almacenes_proyecto(warehouse_id); +CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_fraccionamiento_id ON inventory.almacenes_proyecto(fraccionamiento_id); + +CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_tenant_id ON inventory.requisiciones_obra(tenant_id); +CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_fraccionamiento_id ON inventory.requisiciones_obra(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_status ON inventory.requisiciones_obra(status); +CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_date ON inventory.requisiciones_obra(requisition_date); +CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_required_date ON inventory.requisiciones_obra(required_date); + +CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_tenant_id ON inventory.requisicion_lineas(tenant_id); +CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_requisicion_id ON inventory.requisicion_lineas(requisicion_id); +CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_product_id ON inventory.requisicion_lineas(product_id); + +CREATE INDEX IF NOT EXISTS idx_consumos_obra_tenant_id ON inventory.consumos_obra(tenant_id); +CREATE INDEX IF NOT EXISTS idx_consumos_obra_fraccionamiento_id ON inventory.consumos_obra(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_consumos_obra_lote_id ON inventory.consumos_obra(lote_id); +CREATE INDEX IF NOT EXISTS idx_consumos_obra_concepto_id ON inventory.consumos_obra(concepto_id); +CREATE INDEX IF NOT EXISTS idx_consumos_obra_product_id ON inventory.consumos_obra(product_id); +CREATE INDEX IF NOT EXISTS idx_consumos_obra_date ON inventory.consumos_obra(consumption_date); + +-- ============================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================================ + +ALTER TABLE inventory.almacenes_proyecto ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.requisiciones_obra ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.requisicion_lineas ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.consumos_obra ENABLE ROW LEVEL SECURITY; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto; + CREATE POLICY tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto + 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_requisiciones_obra ON inventory.requisiciones_obra; + CREATE POLICY tenant_isolation_requisiciones_obra ON inventory.requisiciones_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_requisicion_lineas ON inventory.requisicion_lineas; + CREATE POLICY tenant_isolation_requisicion_lineas ON inventory.requisicion_lineas + 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_consumos_obra ON inventory.consumos_obra; + CREATE POLICY tenant_isolation_consumos_obra ON inventory.consumos_obra + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON TABLE inventory.almacenes_proyecto IS 'Extensión: almacenes por proyecto de construcción'; +COMMENT ON TABLE inventory.requisiciones_obra IS 'Extensión: requisiciones de material desde obra'; +COMMENT ON TABLE inventory.requisicion_lineas IS 'Extensión: líneas de requisición de obra'; +COMMENT ON TABLE inventory.consumos_obra IS 'Extensión: consumos de materiales por obra/lote'; + +-- ============================================================================ +-- FIN DE EXTENSIONES INVENTORY +-- Total tablas: 4 +-- ============================================================================ diff --git a/schemas/07-purchase-ext-schema-ddl.sql b/schemas/07-purchase-ext-schema-ddl.sql new file mode 100644 index 0000000..03d6a43 --- /dev/null +++ b/schemas/07-purchase-ext-schema-ddl.sql @@ -0,0 +1,227 @@ +-- ============================================================================ +-- PURCHASE EXTENSION Schema DDL - Extensiones de Compras para Construcción +-- Modulos: MAI-004 (Compras e Inventarios) +-- Version: 1.0.0 +-- Fecha: 2025-12-08 +-- ============================================================================ +-- TIPO: Extensión del ERP Core (MGN-006 Purchase) +-- NOTA: Contiene SOLO extensiones específicas de construcción. +-- Las tablas base están en el ERP Core. +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth.tenants, auth.users) +-- 2. Schema construction instalado (fraccionamientos) +-- 3. Schema inventory extension instalado (requisiciones_obra) +-- 4. Schema purchase de ERP-Core instalado (opcional, para FKs) +-- ============================================================================ + +-- Verificar prerequisitos +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_namespace WHERE nspname = 'construction') THEN + RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'inventory') THEN + RAISE EXCEPTION 'Schema inventory no existe. Ejecutar primero inventory extension DDL'; + END IF; +END $$; + +-- Crear schema si no existe (puede ya existir desde ERP-Core) +CREATE SCHEMA IF NOT EXISTS purchase; + +-- ============================================================================ +-- TABLES - EXTENSIONES CONSTRUCCIÓN +-- ============================================================================ + +-- Tabla: purchase_order_construction (extensión de órdenes de compra) +-- Extiende: purchase.purchase_orders (ERP Core) +CREATE TABLE IF NOT EXISTS purchase.purchase_order_construction ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + purchase_order_id UUID NOT NULL, -- FK a purchase.purchase_orders (ERP Core) + fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), + requisicion_id UUID REFERENCES inventory.requisiciones_obra(id), + delivery_location VARCHAR(255), + delivery_contact VARCHAR(100), + delivery_phone VARCHAR(20), + received_by UUID REFERENCES auth.users(id), + received_at TIMESTAMPTZ, + quality_approved BOOLEAN, + quality_notes TEXT, + 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_po_construction_po_id UNIQUE (purchase_order_id) +); + +-- Tabla: supplier_construction (extensión de proveedores) +-- Extiende: purchase.suppliers (ERP Core) +CREATE TABLE IF NOT EXISTS purchase.supplier_construction ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + supplier_id UUID NOT NULL, -- FK a purchase.suppliers (ERP Core) + is_materials_supplier BOOLEAN DEFAULT FALSE, + is_services_supplier BOOLEAN DEFAULT FALSE, + is_equipment_supplier BOOLEAN DEFAULT FALSE, + specialties TEXT[], + quality_rating DECIMAL(3,2), + delivery_rating DECIMAL(3,2), + price_rating DECIMAL(3,2), + overall_rating DECIMAL(3,2) GENERATED ALWAYS AS ( + (COALESCE(quality_rating, 0) + COALESCE(delivery_rating, 0) + COALESCE(price_rating, 0)) / 3 + ) STORED, + last_evaluation_date DATE, + credit_limit DECIMAL(14,2), + payment_days INTEGER DEFAULT 30, + has_valid_documents BOOLEAN DEFAULT FALSE, + documents_expiry_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_supplier_construction_supplier_id UNIQUE (supplier_id) +); + +-- Tabla: comparativo_cotizaciones (cuadro comparativo) +CREATE TABLE IF NOT EXISTS purchase.comparativo_cotizaciones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + requisicion_id UUID REFERENCES inventory.requisiciones_obra(id), + code VARCHAR(30) NOT NULL, + name VARCHAR(255) NOT NULL, + comparison_date DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'draft', + winner_supplier_id UUID, -- FK a purchase.suppliers (ERP Core) + approved_by UUID REFERENCES auth.users(id), + approved_at TIMESTAMPTZ, + notes TEXT, + 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_comparativo_code_tenant UNIQUE (tenant_id, code) +); + +-- Tabla: comparativo_proveedores (proveedores en comparativo) +CREATE TABLE IF NOT EXISTS purchase.comparativo_proveedores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + comparativo_id UUID NOT NULL REFERENCES purchase.comparativo_cotizaciones(id) ON DELETE CASCADE, + supplier_id UUID NOT NULL, -- FK a purchase.suppliers (ERP Core) + quotation_number VARCHAR(50), + quotation_date DATE, + delivery_days INTEGER, + payment_conditions VARCHAR(100), + total_amount DECIMAL(16,2), + is_selected BOOLEAN DEFAULT FALSE, + evaluation_notes TEXT, + 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: comparativo_productos (productos en comparativo) +CREATE TABLE IF NOT EXISTS purchase.comparativo_productos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + comparativo_proveedor_id UUID NOT NULL REFERENCES purchase.comparativo_proveedores(id) ON DELETE CASCADE, + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + quantity DECIMAL(12,4) NOT NULL, + unit_price DECIMAL(12,4) NOT NULL, + total_price DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_po_construction_tenant_id ON purchase.purchase_order_construction(tenant_id); +CREATE INDEX IF NOT EXISTS idx_po_construction_po_id ON purchase.purchase_order_construction(purchase_order_id); +CREATE INDEX IF NOT EXISTS idx_po_construction_fraccionamiento_id ON purchase.purchase_order_construction(fraccionamiento_id); +CREATE INDEX IF NOT EXISTS idx_po_construction_requisicion_id ON purchase.purchase_order_construction(requisicion_id); + +CREATE INDEX IF NOT EXISTS idx_supplier_construction_tenant_id ON purchase.supplier_construction(tenant_id); +CREATE INDEX IF NOT EXISTS idx_supplier_construction_supplier_id ON purchase.supplier_construction(supplier_id); +CREATE INDEX IF NOT EXISTS idx_supplier_construction_rating ON purchase.supplier_construction(overall_rating); + +CREATE INDEX IF NOT EXISTS idx_comparativo_tenant_id ON purchase.comparativo_cotizaciones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_comparativo_requisicion_id ON purchase.comparativo_cotizaciones(requisicion_id); +CREATE INDEX IF NOT EXISTS idx_comparativo_status ON purchase.comparativo_cotizaciones(status); + +CREATE INDEX IF NOT EXISTS idx_comparativo_prov_tenant_id ON purchase.comparativo_proveedores(tenant_id); +CREATE INDEX IF NOT EXISTS idx_comparativo_prov_comparativo_id ON purchase.comparativo_proveedores(comparativo_id); +CREATE INDEX IF NOT EXISTS idx_comparativo_prov_supplier_id ON purchase.comparativo_proveedores(supplier_id); + +CREATE INDEX IF NOT EXISTS idx_comparativo_prod_tenant_id ON purchase.comparativo_productos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_comparativo_prod_proveedor_id ON purchase.comparativo_productos(comparativo_proveedor_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================================ + +ALTER TABLE purchase.purchase_order_construction ENABLE ROW LEVEL SECURITY; +ALTER TABLE purchase.supplier_construction ENABLE ROW LEVEL SECURITY; +ALTER TABLE purchase.comparativo_cotizaciones ENABLE ROW LEVEL SECURITY; +ALTER TABLE purchase.comparativo_proveedores ENABLE ROW LEVEL SECURITY; +ALTER TABLE purchase.comparativo_productos ENABLE ROW LEVEL SECURITY; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_po_construction ON purchase.purchase_order_construction; + CREATE POLICY tenant_isolation_po_construction ON purchase.purchase_order_construction + 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_supplier_construction ON purchase.supplier_construction; + CREATE POLICY tenant_isolation_supplier_construction ON purchase.supplier_construction + 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_comparativo ON purchase.comparativo_cotizaciones; + CREATE POLICY tenant_isolation_comparativo ON purchase.comparativo_cotizaciones + 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_comparativo_prov ON purchase.comparativo_proveedores; + CREATE POLICY tenant_isolation_comparativo_prov ON purchase.comparativo_proveedores + 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_comparativo_prod ON purchase.comparativo_productos; + CREATE POLICY tenant_isolation_comparativo_prod ON purchase.comparativo_productos + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON TABLE purchase.purchase_order_construction IS 'Extensión: datos adicionales de OC para construcción'; +COMMENT ON TABLE purchase.supplier_construction IS 'Extensión: datos adicionales de proveedores para construcción'; +COMMENT ON TABLE purchase.comparativo_cotizaciones IS 'Extensión: cuadro comparativo de cotizaciones'; +COMMENT ON TABLE purchase.comparativo_proveedores IS 'Extensión: proveedores participantes en comparativo'; +COMMENT ON TABLE purchase.comparativo_productos IS 'Extensión: productos cotizados por proveedor'; + +-- ============================================================================ +-- FIN DE EXTENSIONES PURCHASE +-- Total tablas: 5 +-- ============================================================================ diff --git a/validate-clean-load-policy.sh b/validate-clean-load-policy.sh new file mode 100755 index 0000000..0a1ad76 --- /dev/null +++ b/validate-clean-load-policy.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# ============================================================================= +# VALIDATE CLEAN LOAD POLICY +# ============================================================================= +# Script de validacion de cumplimiento de DIRECTIVA-POLITICA-CARGA-LIMPIA.md +# +# Uso: ./validate-clean-load-policy.sh +# ============================================================================= + +set -e + +# Colores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Configuracion +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VIOLATIONS=0 + +echo -e "${BLUE}=============================================================================${NC}" +echo -e "${BLUE} VALIDACION DE POLITICA DE CARGA LIMPIA${NC}" +echo -e "${BLUE}=============================================================================${NC}" +echo "" + +# ============================================================================= +# CHECK 1: No debe existir carpeta migrations/ +# ============================================================================= +echo -e "${YELLOW}[1/6] Verificando que NO existe carpeta migrations/...${NC}" + +if [ -d "$SCRIPT_DIR/migrations" ]; then + echo -e "${RED}ERROR: Carpeta migrations/ detectada (PROHIBIDA)${NC}" + echo -e "${RED} Path: $SCRIPT_DIR/migrations${NC}" + echo -e "${YELLOW} Solucion: Eliminar carpeta y mover contenido a schemas/${NC}" + VIOLATIONS=$((VIOLATIONS + 1)) +else + echo -e "${GREEN}OK - No existe carpeta migrations/${NC}" +fi +echo "" + +# ============================================================================= +# CHECK 2: No deben existir archivos fix-*.sql +# ============================================================================= +echo -e "${YELLOW}[2/6] Verificando que NO existen archivos fix-*.sql...${NC}" + +FIX_FILES=$(find "$SCRIPT_DIR" -name "fix-*.sql" -o -name "patch-*.sql" -o -name "hotfix-*.sql" 2>/dev/null) + +if [ -n "$FIX_FILES" ]; then + echo -e "${RED}ERROR: Archivos fix/patch detectados (PROHIBIDOS):${NC}" + echo "$FIX_FILES" | while read -r file; do + echo -e "${RED} - $file${NC}" + done + echo -e "${YELLOW} Solucion: Incorporar cambios en DDL base y eliminar fixes${NC}" + VIOLATIONS=$((VIOLATIONS + 1)) +else + echo -e "${GREEN}OK - No existen archivos fix/patch${NC}" +fi +echo "" + +# ============================================================================= +# CHECK 3: No deben existir archivos migration-*.sql o NNN-*.sql numerados +# ============================================================================= +echo -e "${YELLOW}[3/6] Verificando que NO existen archivos tipo migration...${NC}" + +MIGRATION_FILES=$(find "$SCRIPT_DIR" -regex ".*[0-9][0-9][0-9][-_].*\.sql" ! -path "*/schemas/*" 2>/dev/null) + +if [ -n "$MIGRATION_FILES" ]; then + echo -e "${RED}ERROR: Archivos tipo migration detectados (PROHIBIDOS):${NC}" + echo "$MIGRATION_FILES" | while read -r file; do + echo -e "${RED} - $file${NC}" + done + echo -e "${YELLOW} Solucion: Mover a schemas/ con nomenclatura correcta${NC}" + VIOLATIONS=$((VIOLATIONS + 1)) +else + echo -e "${GREEN}OK - No existen archivos tipo migration fuera de schemas/${NC}" +fi +echo "" + +# ============================================================================= +# CHECK 4: Debe existir script drop-and-recreate-database.sh +# ============================================================================= +echo -e "${YELLOW}[4/6] Verificando que existe drop-and-recreate-database.sh...${NC}" + +if [ -f "$SCRIPT_DIR/drop-and-recreate-database.sh" ]; then + if [ -x "$SCRIPT_DIR/drop-and-recreate-database.sh" ]; then + echo -e "${GREEN}OK - Script existe y es ejecutable${NC}" + else + echo -e "${YELLOW}WARN: Script existe pero no es ejecutable${NC}" + echo -e "${YELLOW} Solucion: chmod +x drop-and-recreate-database.sh${NC}" + fi +else + echo -e "${RED}ERROR: No existe drop-and-recreate-database.sh (REQUERIDO)${NC}" + echo -e "${YELLOW} Solucion: Crear script de recreacion limpia${NC}" + VIOLATIONS=$((VIOLATIONS + 1)) +fi +echo "" + +# ============================================================================= +# CHECK 5: Deben existir archivos DDL en schemas/ +# ============================================================================= +echo -e "${YELLOW}[5/6] Verificando que existen archivos DDL en schemas/...${NC}" + +DDL_COUNT=$(find "$SCRIPT_DIR/schemas" -name "*.sql" -type f 2>/dev/null | wc -l) + +if [ "$DDL_COUNT" -gt 0 ]; then + echo -e "${GREEN}OK - Encontrados $DDL_COUNT archivos DDL en schemas/${NC}" + find "$SCRIPT_DIR/schemas" -name "*.sql" -type f | sort | while read -r file; do + echo -e " - $(basename "$file")" + done +else + echo -e "${YELLOW}WARN: No hay archivos DDL en schemas/${NC}" + echo -e "${YELLOW} La base de datos puede quedar vacia${NC}" +fi +echo "" + +# ============================================================================= +# CHECK 6: Debe existir archivo de inicializacion +# ============================================================================= +echo -e "${YELLOW}[6/6] Verificando archivo de inicializacion...${NC}" + +if [ -f "$SCRIPT_DIR/init-scripts/01-init-database.sql" ]; then + echo -e "${GREEN}OK - Existe init-scripts/01-init-database.sql${NC}" +elif [ -f "$SCRIPT_DIR/ddl/00-init.sql" ]; then + echo -e "${GREEN}OK - Existe ddl/00-init.sql${NC}" +else + echo -e "${RED}ERROR: No existe archivo de inicializacion${NC}" + echo -e "${YELLOW} Solucion: Crear init-scripts/01-init-database.sql${NC}" + VIOLATIONS=$((VIOLATIONS + 1)) +fi +echo "" + +# ============================================================================= +# RESUMEN +# ============================================================================= + +echo -e "${BLUE}=============================================================================${NC}" + +if [ "$VIOLATIONS" -eq 0 ]; then + echo -e "${GREEN} POLITICA DE CARGA LIMPIA: CUMPLIDA${NC}" + echo -e "${GREEN}=============================================================================${NC}" + echo -e "${GREEN} Todas las validaciones pasaron correctamente${NC}" + echo -e "${GREEN}=============================================================================${NC}" + exit 0 +else + echo -e "${RED} POLITICA DE CARGA LIMPIA: VIOLADA${NC}" + echo -e "${RED}=============================================================================${NC}" + echo -e "${RED} Se encontraron $VIOLATIONS violacion(es)${NC}" + echo -e "${RED} Revisar DIRECTIVA-POLITICA-CARGA-LIMPIA.md para corregir${NC}" + echo -e "${RED}=============================================================================${NC}" + exit 1 +fi