Migración desde erp-construccion/database - Estándar multi-repo v2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-16 08:11:21 -06:00
parent 5744bc50ed
commit bf97e26cdf
14 changed files with 5428 additions and 2 deletions

362
HERENCIA-ERP-CORE.md Normal file
View File

@ -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

186
README.md
View File

@ -1,3 +1,185 @@
# erp-construccion-database-v2 # Database - MVP Sistema Administración de Obra
Database de erp-construccion - Workspace V2 **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

View File

@ -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.**

306
_MAP.md Normal file
View File

@ -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

188
drop-and-recreate-database.sh Executable file
View File

@ -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}"

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

File diff suppressed because it is too large Load Diff

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

153
validate-clean-load-policy.sh Executable file
View File

@ -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