From 5f009673aaeb67078aa31cf052010b63ef7cf40c Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Thu, 5 Feb 2026 23:18:19 -0600 Subject: [PATCH] [REMEDIATION] feat: Database schema updates and recreate script enhancement Major overhaul of drop-and-recreate-database.sh, DDL schema updates, seed data cleanup. Add utility scripts for auth table fixes. Co-Authored-By: Claude Opus 4.6 --- drop-and-recreate-database.sh | 972 +++++++++++++++++++++---- fix_auth_tables.sql | 70 ++ insert_admin_user.sql | 21 + schemas/01-construction-schema-ddl.sql | 42 +- seeds/dev/03-empresa-tenant.sql | 200 +---- seeds/dev/04-proyectos-obras.sql | 2 +- temp_update_hash.sql | 1 + update_password.sql | 1 + 8 files changed, 1011 insertions(+), 298 deletions(-) create mode 100644 fix_auth_tables.sql create mode 100644 insert_admin_user.sql create mode 100644 temp_update_hash.sql create mode 100644 update_password.sql diff --git a/drop-and-recreate-database.sh b/drop-and-recreate-database.sh index b679265..923eeab 100755 --- a/drop-and-recreate-database.sh +++ b/drop-and-recreate-database.sh @@ -2,187 +2,901 @@ # ============================================================================= # DROP AND RECREATE DATABASE - ERP CONSTRUCCION # ============================================================================= -# Script de carga limpia segun DIRECTIVA-POLITICA-CARGA-LIMPIA.md +# Script de carga limpia con soporte para seeds por ambiente # -# Uso: ./drop-and-recreate-database.sh [DATABASE_URL] -# Ejemplo: ./drop-and-recreate-database.sh "postgresql://user:pass@localhost:5433/erp_construccion" +# Uso: ./drop-and-recreate-database.sh [opciones] [DATABASE_URL] +# +# Opciones: +# -s, --seeds Cargar seeds despues de DDL +# -e, --env ENV Ambiente: dev|staging|prod (default: dev) +# -f, --force No pedir confirmacion +# -v, --verbose Modo verbose +# -h, --help Mostrar ayuda +# +# Ejemplos: +# ./drop-and-recreate-database.sh --seeds +# ./drop-and-recreate-database.sh --seeds --env dev +# ./drop-and-recreate-database.sh -s -f "postgresql://user:pass@localhost:5432/mydb" +# +# Referencia: @SIMCO-DDL-UNIFIED, @TRIGGER-DDL-WSL # ============================================================================= set -e +# ============================================================================= +# CONFIGURACION +# ============================================================================= + # Colores RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +CYAN='\033[0;36m' +NC='\033[0m' -# Configuracion +# Directorios 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" +SEEDS_DIR="$SCRIPT_DIR/seeds" -# Database URL (por defecto desarrollo local) -DATABASE_URL="${1:-${DATABASE_URL:-postgresql://postgres:postgres@localhost:5433/erp_construccion}}" +# Valores por defecto +LOAD_SEEDS=false +ENVIRONMENT="dev" +FORCE=false +VERBOSE=false +DATABASE_URL="" -# 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 "" +# Credenciales por defecto (segun WORKSPACE-INTEGRATION.yml) +DEFAULT_HOST="localhost" +DEFAULT_PORT="5432" +DEFAULT_DB="erp_construccion_demo" +DEFAULT_USER="erp_admin" +DEFAULT_PASS="erp_dev_2026" # ============================================================================= -# PASO 1: Verificar conexion +# FUNCIONES DE UTILIDAD # ============================================================================= -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 "" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } + +show_help() { + cat << EOF +Uso: $0 [opciones] [DATABASE_URL] + +Script de recreacion de base de datos con soporte para seeds. + +Opciones: + -s, --seeds Cargar seeds despues del DDL + -e, --env ENV Ambiente para seeds: dev|staging|prod (default: dev) + -f, --force No pedir confirmacion para DROP + -v, --verbose Mostrar output detallado + -h, --help Mostrar esta ayuda + +DATABASE_URL: + Formato: postgresql://user:pass@host:port/database + Default: postgresql://$DEFAULT_USER:****@$DEFAULT_HOST:$DEFAULT_PORT/$DEFAULT_DB + +Ejemplos: + $0 --seeds # Recrear BD y cargar seeds dev + $0 --seeds --env staging # Recrear BD con seeds staging + $0 -s -f # Recrear sin confirmacion + $0 "postgresql://..." # Usar URL personalizada + +Seeds disponibles por ambiente: + dev/ - Datos completos para desarrollo (usuarios demo, proyectos, etc.) + staging/ - Datos minimos para pruebas + prod/ - Solo catalogos base (sin datos de prueba) + +EOF +} # ============================================================================= -# PASO 2: DROP schemas existentes (carga limpia) +# PARSEO DE ARGUMENTOS # ============================================================================= -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" -) +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -s|--seeds) + LOAD_SEEDS=true + shift + ;; + -e|--env) + ENVIRONMENT="$2" + shift 2 + ;; + -f|--force) + FORCE=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + postgresql://*) + DATABASE_URL="$1" + shift + ;; + *) + log_error "Opcion desconocida: $1" + show_help + exit 1 + ;; + esac + done -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" + # Construir DATABASE_URL si no se proporciono + if [ -z "$DATABASE_URL" ]; then + DATABASE_URL="postgresql://$DEFAULT_USER:$DEFAULT_PASS@$DEFAULT_HOST:$DEFAULT_PORT/$DEFAULT_DB" fi -done -echo -e "${GREEN}OK - Schemas eliminados${NC}" -echo "" + + # Validar ambiente + if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|prod)$ ]]; then + log_error "Ambiente invalido: $ENVIRONMENT (usar: dev|staging|prod)" + exit 1 + fi +} # ============================================================================= -# PASO 3: Ejecutar DDL inicial +# FUNCIONES DE BASE DE DATOS # ============================================================================= -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 "" +run_sql() { + local sql="$1" + if [ "$VERBOSE" = true ]; then + psql "$DATABASE_URL" -c "$sql" + else + psql "$DATABASE_URL" -c "$sql" > /dev/null 2>&1 + fi +} + +run_sql_file() { + local file="$1" + if [ "$VERBOSE" = true ]; then + psql "$DATABASE_URL" -f "$file" + else + psql "$DATABASE_URL" -f "$file" > /dev/null 2>&1 + fi +} + +run_sql_quiet() { + local sql="$1" + psql "$DATABASE_URL" -t -A -c "$sql" 2>/dev/null | tr -d ' \n' +} # ============================================================================= -# PASO 4: Ejecutar DDL de schemas modulares +# PASO 0: VERIFICACIONES INICIALES # ============================================================================= -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) +verify_connection() { + log_step "Verificando conexion a PostgreSQL..." -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 + if ! psql "$DATABASE_URL" -c "SELECT 1" > /dev/null 2>&1; then + log_error "No se puede conectar a PostgreSQL" + log_info "URL: $DATABASE_URL" + log_info "Verificar que PostgreSQL esta corriendo y las credenciales son correctas" + exit 1 + fi + + log_success "Conexion establecida" +} + +confirm_drop() { + if [ "$FORCE" = true ]; then + return 0 + fi + + local db_name=$(echo "$DATABASE_URL" | sed -E 's|.*/([^?]+).*|\1|') + + echo "" + echo -e "${RED}ADVERTENCIA: Esto eliminara TODOS los datos de: $db_name${NC}" + echo "" + read -p "Escribe 'SI' para confirmar: " confirm + + if [ "$confirm" != "SI" ]; then + log_info "Operacion cancelada" + exit 0 + fi +} + +# ============================================================================= +# PASO 1: DROP SCHEMAS EXISTENTES +# ============================================================================= + +drop_schemas() { + log_step "Eliminando schemas existentes..." + + # Schemas a eliminar (orden inverso de dependencias) + local schemas=( + "documents" "assets" "finance" "infonavit" "hse" "hr" + "purchase" "inventory" "estimates" "construction" + "tenants" "core_shared" "core" "auth" + # Legacy schemas + "infonavit_management" "safety_management" "quality_management" + "construction_management" "inventory_management" "purchasing_management" + "financial_management" "project_management" "auth_management" + ) + + for schema in "${schemas[@]}"; do + if [ "$(run_sql_quiet "SELECT 1 FROM pg_namespace WHERE nspname = '$schema'")" = "1" ]; then + run_sql "DROP SCHEMA IF EXISTS $schema CASCADE" + [ "$VERBOSE" = true ] && log_info "Schema $schema eliminado" + fi + done + + log_success "Schemas eliminados" +} + +# ============================================================================= +# PASO 2: CREAR SCHEMAS BASE (auth, core) +# ============================================================================= + +create_base_schemas() { + log_step "Creando schemas base (auth, core)..." + + # auth y core deben existir antes de ejecutar los DDLs + psql "$DATABASE_URL" << 'EOSQL' > /dev/null 2>&1 +-- Crear schemas base que son prerequisito +CREATE SCHEMA IF NOT EXISTS auth; +CREATE SCHEMA IF NOT EXISTS core; +CREATE SCHEMA IF NOT EXISTS core_shared; +CREATE SCHEMA IF NOT EXISTS tenants; + +-- Tabla auth.tenants (necesaria para FK) +CREATE TABLE IF NOT EXISTS auth.tenants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + settings JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabla auth.users (necesaria para FK) +CREATE TABLE IF NOT EXISTS auth.users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID REFERENCES auth.tenants(id), + email VARCHAR(255) NOT NULL, + username VARCHAR(100), + is_active BOOLEAN DEFAULT true, + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON SCHEMA auth IS 'Schema de autenticacion - tablas base para FK'; +COMMENT ON SCHEMA core IS 'Schema de catalogos core'; +EOSQL + + log_success "Schemas base creados" +} + +# ============================================================================= +# PASO 3: EJECUTAR DDL INICIAL +# ============================================================================= + +execute_init_ddl() { + log_step "Ejecutando DDL inicial..." + + if [ -f "$INIT_SCRIPTS_DIR/01-init-database.sql" ]; then + run_sql_file "$INIT_SCRIPTS_DIR/01-init-database.sql" + log_success "01-init-database.sql ejecutado" + else + log_error "No se encontro archivo de inicializacion" + exit 1 + fi +} + +# ============================================================================= +# PASO 4: EJECUTAR DDL DE SCHEMAS +# ============================================================================= + +execute_schema_ddl() { + log_step "Ejecutando DDL de schemas..." + + if [ ! -d "$SCHEMAS_DIR" ]; then + log_warning "Directorio schemas/ no encontrado" + return 0 + fi + + # Ejecutar en orden numerico + for ddl_file in $(ls -1 "$SCHEMAS_DIR"/*.sql 2>/dev/null | sort -V); do + local filename=$(basename "$ddl_file") + echo -ne " - ${YELLOW}$filename${NC}..." + + if run_sql_file "$ddl_file"; then echo -e " ${GREEN}OK${NC}" else echo -e " ${RED}ERROR${NC}" - echo -e "${RED}Fallo al ejecutar: $ddl_file${NC}" + log_error "Fallo al ejecutar: $ddl_file" exit 1 fi done -fi -echo "" + + log_success "DDL de schemas completado" +} # ============================================================================= -# PASO 5: Ejecutar DDL legacy (si existe) +# PASO 4: CREAR TABLAS COMPLEMENTARIAS (para seeds) # ============================================================================= -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 +create_complementary_tables() { + log_step "Creando tablas complementarias de erp-core..." + + # Estas tablas son necesarias para que los seeds funcionen + # cuando se ejecuta standalone (sin erp-core completo) + + psql "$DATABASE_URL" << 'EOSQL' > /dev/null 2>&1 +-- ============================================================ +-- TABLAS COMPLEMENTARIAS PARA SEEDS +-- Estas tablas normalmente vienen de erp-core +-- ============================================================ + +-- Schema tenants +CREATE SCHEMA IF NOT EXISTS tenants; + +-- tenants.tenants (estructura completa) +CREATE TABLE IF NOT EXISTS tenants.tenants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + legal_name VARCHAR(255), + rfc VARCHAR(20), + business_type VARCHAR(100), + status VARCHAR(20) DEFAULT 'active', + address_street TEXT, + address_city VARCHAR(100), + address_state VARCHAR(100), + address_zip VARCHAR(20), + address_country VARCHAR(100) DEFAULT 'Mexico', + phone VARCHAR(50), + email VARCHAR(255), + website VARCHAR(255), + logo_url VARCHAR(500), + primary_color VARCHAR(10), + secondary_color VARCHAR(10), + fiscal_regime VARCHAR(10), + cfdi_certificate_number VARCHAR(50), + settings JSONB DEFAULT '{}', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID, + updated_at TIMESTAMPTZ DEFAULT NOW(), + updated_by UUID +); + +-- tenants.tenant_settings +CREATE TABLE IF NOT EXISTS tenants.tenant_settings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID REFERENCES tenants.tenants(id), + setting_key VARCHAR(100) NOT NULL, + setting_value JSONB, + setting_type VARCHAR(50), + description TEXT, + is_system BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- tenants.branches +CREATE TABLE IF NOT EXISTS tenants.branches ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID REFERENCES tenants.tenants(id), + code VARCHAR(50) NOT NULL, + name VARCHAR(200) NOT NULL, + address TEXT, + city VARCHAR(100), + state VARCHAR(100), + phone VARCHAR(50), + is_main BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID, + UNIQUE(tenant_id, code) +); + +-- auth.tenants (si no existe) +CREATE TABLE IF NOT EXISTS auth.tenants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + settings JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- auth.users (extender si existe) +DO $$ BEGIN + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS middle_name VARCHAR(100); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS phone VARCHAR(50); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(500); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS position VARCHAR(100); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS department VARCHAR(100); + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS role_id UUID; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS language VARCHAR(10) DEFAULT 'es'; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS timezone VARCHAR(50) DEFAULT 'America/Mexico_City'; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS is_verified BOOLEAN DEFAULT false; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS is_system BOOLEAN DEFAULT false; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS failed_login_attempts INT DEFAULT 0; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS locked_until TIMESTAMPTZ; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN DEFAULT false; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS created_by UUID; + ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS settings JSONB DEFAULT '{}'; +EXCEPTION WHEN undefined_table THEN NULL; END $$; + +-- auth.roles +CREATE TABLE IF NOT EXISTS auth.roles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID, + code VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + permissions JSONB DEFAULT '[]', + is_system BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(tenant_id, code) +); + +-- auth.permissions +CREATE TABLE IF NOT EXISTS auth.permissions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID, + code VARCHAR(100) NOT NULL, + name VARCHAR(200) NOT NULL, + description TEXT, + module VARCHAR(50), + action VARCHAR(50), + resource VARCHAR(100), + is_system BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(tenant_id, code) +); + +-- auth.user_roles +CREATE TABLE IF NOT EXISTS auth.user_roles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID, + role_id UUID, + tenant_id UUID, + is_primary BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, role_id) +); + +-- auth.role_permissions +CREATE TABLE IF NOT EXISTS auth.role_permissions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + role_id UUID, + permission_id UUID, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(role_id, permission_id) +); + +-- core.units (estructura completa) +DROP TABLE IF EXISTS core.units CASCADE; +CREATE TABLE core.units ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID, + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + symbol VARCHAR(10), + abbreviation VARCHAR(10), + category VARCHAR(50), + unit_type VARCHAR(50), + description TEXT, + base_unit_id UUID, + conversion_factor DECIMAL(18,6) DEFAULT 1, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID, + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(code) +); + +-- core.currencies +CREATE TABLE IF NOT EXISTS core.currencies ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(3) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + symbol VARCHAR(10), + decimal_places INT DEFAULT 2, + is_active BOOLEAN DEFAULT true, + is_default BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.taxes +CREATE TABLE IF NOT EXISTS core.taxes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + rate DECIMAL(5,4) NOT NULL, + tax_type VARCHAR(20), + is_retention BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.payment_methods +CREATE TABLE IF NOT EXISTS core.payment_methods ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + requires_reference BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.banks +CREATE TABLE IF NOT EXISTS core.banks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + short_name VARCHAR(50), + swift_code VARCHAR(20), + country VARCHAR(50) DEFAULT 'Mexico', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.states +CREATE TABLE IF NOT EXISTS core.states ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(10) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + abbreviation VARCHAR(10), + country VARCHAR(50) DEFAULT 'Mexico', + country_code VARCHAR(3) DEFAULT 'MX', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.municipalities +CREATE TABLE IF NOT EXISTS core.municipalities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + state_id UUID REFERENCES core.states(id), + code VARCHAR(10), + name VARCHAR(200), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.material_categories +CREATE TABLE IF NOT EXISTS core.material_categories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + parent_id UUID, + description TEXT, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.cost_types +CREATE TABLE IF NOT EXISTS core.cost_types ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + factor DECIMAL(5,2), + is_direct BOOLEAN DEFAULT true, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- core.document_types +CREATE TABLE IF NOT EXISTS core.document_types ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + category VARCHAR(50), + requires_signature BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Columnas adicionales para tablas existentes +DO $$ BEGIN + -- construction.fraccionamientos + ALTER TABLE construction.fraccionamientos ADD COLUMN IF NOT EXISTS project_type VARCHAR(50); + ALTER TABLE construction.fraccionamientos ADD COLUMN IF NOT EXISTS estimated_end_date DATE; + ALTER TABLE construction.fraccionamientos ADD COLUMN IF NOT EXISTS budget DECIMAL(18,2); + ALTER TABLE construction.fraccionamientos ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true; + + -- construction.conceptos + ALTER TABLE construction.conceptos ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true; + + -- hse.incidentes + ALTER TABLE hse.incidentes ADD COLUMN IF NOT EXISTS acciones_inmediatas TEXT; + ALTER TABLE hse.incidentes ADD COLUMN IF NOT EXISTS investigado_por UUID; + ALTER TABLE hse.incidentes ADD COLUMN IF NOT EXISTS fecha_investigacion DATE; + ALTER TABLE hse.incidentes ADD COLUMN IF NOT EXISTS cerrado_por UUID; + ALTER TABLE hse.incidentes ADD COLUMN IF NOT EXISTS fecha_cierre DATE; + + -- hse.capacitaciones + ALTER TABLE hse.capacitaciones ADD COLUMN IF NOT EXISTS puntaje_minimo INTEGER; + + -- hse.horas_trabajadas + ALTER TABLE hse.horas_trabajadas ADD COLUMN IF NOT EXISTS trabajadores_directos INTEGER; + ALTER TABLE hse.horas_trabajadas ADD COLUMN IF NOT EXISTS trabajadores_indirectos INTEGER; + + -- assets.assets + ALTER TABLE assets.assets ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true; + + -- assets.maintenance_plans + ALTER TABLE assets.maintenance_plans ADD COLUMN IF NOT EXISTS estimated_cost DECIMAL(18,2); + ALTER TABLE assets.maintenance_plans ADD COLUMN IF NOT EXISTS actual_cost DECIMAL(18,2); + ALTER TABLE assets.maintenance_plans ADD COLUMN IF NOT EXISTS tasks_description TEXT; + ALTER TABLE assets.maintenance_plans ADD COLUMN IF NOT EXISTS last_performed_at TIMESTAMPTZ; + ALTER TABLE assets.maintenance_plans ADD COLUMN IF NOT EXISTS next_due_at TIMESTAMPTZ; + ALTER TABLE assets.maintenance_plans ALTER COLUMN activities DROP NOT NULL; +EXCEPTION WHEN undefined_table THEN NULL; END $$; + +EOSQL + + log_success "Tablas complementarias creadas" +} + +# ============================================================================= +# PASO 5: OTORGAR PERMISOS +# ============================================================================= + +grant_permissions() { + log_step "Otorgando permisos..." + + local db_user=$(echo "$DATABASE_URL" | sed -E 's|.*://([^:]+):.*|\1|') + + psql "$DATABASE_URL" << EOSQL > /dev/null 2>&1 +-- Permisos sobre schemas +GRANT ALL ON SCHEMA auth TO $db_user; +GRANT ALL ON SCHEMA core TO $db_user; +GRANT ALL ON SCHEMA core_shared TO $db_user; +GRANT ALL ON SCHEMA tenants TO $db_user; +GRANT ALL ON SCHEMA construction TO $db_user; +GRANT ALL ON SCHEMA estimates TO $db_user; +GRANT ALL ON SCHEMA infonavit TO $db_user; +GRANT ALL ON SCHEMA hse TO $db_user; +GRANT ALL ON SCHEMA hr TO $db_user; +GRANT ALL ON SCHEMA inventory TO $db_user; +GRANT ALL ON SCHEMA purchase TO $db_user; +GRANT ALL ON SCHEMA finance TO $db_user; +GRANT ALL ON SCHEMA assets TO $db_user; +GRANT ALL ON SCHEMA documents TO $db_user; + +-- Permisos sobre tablas +GRANT ALL ON ALL TABLES IN SCHEMA auth TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA core TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA core_shared TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA tenants TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA construction TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA estimates TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA infonavit TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA hse TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA hr TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA inventory TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA purchase TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA finance TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA assets TO $db_user; +GRANT ALL ON ALL TABLES IN SCHEMA documents TO $db_user; + +-- Permisos sobre secuencias +GRANT ALL ON ALL SEQUENCES IN SCHEMA auth TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA core TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA construction TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA estimates TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA infonavit TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA hse TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA hr TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA inventory TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA purchase TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA finance TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA assets TO $db_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA documents TO $db_user; + +-- Permisos por defecto +ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT ALL ON TABLES TO $db_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT ALL ON TABLES TO $db_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA construction GRANT ALL ON TABLES TO $db_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA estimates GRANT ALL ON TABLES TO $db_user; +EOSQL + + log_success "Permisos otorgados" +} + +# ============================================================================= +# PASO 6: CARGAR SEEDS +# ============================================================================= + +load_seeds() { + local env_dir="$SEEDS_DIR/$ENVIRONMENT" + + if [ ! -d "$env_dir" ]; then + log_warning "Directorio de seeds no encontrado: $env_dir" + return 0 + fi + + log_step "Cargando seeds ($ENVIRONMENT)..." + + # Seeds en orden de dependencias (las tablas complementarias ya existen) + local seeds=( + "01-core-catalogs.sql" + "02-users-profiles.sql" + "03-empresa-tenant.sql" + "04-proyectos-obras.sql" + "02b-hr-employees.sql" + "05-presupuestos.sql" + "06-estimaciones.sql" + "07-avances-calidad.sql" + "08-hse.sql" + "09-finanzas.sql" + "10-activos-documentos.sql" + "11-infonavit.sql" + ) + + local success=0 + local failed=0 + local failed_seeds="" + + for seed in "${seeds[@]}"; do + local seed_file="$env_dir/$seed" + + if [ ! -f "$seed_file" ]; then + [ "$VERBOSE" = true ] && log_warning "Seed no encontrado: $seed" + continue + fi + + echo -ne " - ${YELLOW}$seed${NC}..." + + # Ejecutar seed (las transacciones estan dentro de cada archivo) + local output=$(psql "$DATABASE_URL" -f "$seed_file" 2>&1) + + if echo "$output" | grep -qE "(^COMMIT|^INSERT|^DO)"; then echo -e " ${GREEN}OK${NC}" + success=$((success + 1)) else - echo -e " ${YELLOW}SKIP (puede requerir dependencias)${NC}" + echo -e " ${RED}FAIL${NC}" + failed=$((failed + 1)) + failed_seeds="$failed_seeds $seed" + if [ "$VERBOSE" = true ]; then + echo "$output" | grep -E "(^ERROR|violates)" | head -3 + fi fi done -else - echo -e " No hay DDL legacy" -fi -echo "" + + echo "" + if [ $failed -eq 0 ]; then + log_success "Todos los seeds cargados ($success/$success)" + else + log_warning "Seeds cargados: $success exitosos, $failed fallidos" + [ "$VERBOSE" = true ] && log_info "Fallidos:$failed_seeds" + fi +} # ============================================================================= -# PASO 6: Verificar resultado +# PASO 7: 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 ' ') +verify_result() { + log_step "Verificando resultado..." -# 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 ' ') + local schema_count=$(run_sql_quiet " + SELECT COUNT(*) FROM pg_namespace + WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public') + AND nspname NOT LIKE 'pg_%' + ") -# 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; -" + local table_count=$(run_sql_quiet " + SELECT COUNT(*) FROM pg_tables + WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public') + ") -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}" + echo "" + echo -e "${GREEN}=== RESUMEN ===${NC}" + 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; + " + + if [ "$LOAD_SEEDS" = true ]; then + echo "" + echo -e "${CYAN}=== DATOS CARGADOS ===${NC}" + psql "$DATABASE_URL" -c " + SELECT 'auth.users' as tabla, COUNT(*) as registros FROM auth.users + UNION ALL SELECT 'auth.roles', COUNT(*) FROM auth.roles + UNION ALL SELECT 'core.units', COUNT(*) FROM core.units + UNION ALL SELECT 'construction.conceptos', COUNT(*) FROM construction.conceptos + UNION ALL SELECT 'hr.employees', COUNT(*) FROM hr.employees + ORDER BY tabla; + " 2>/dev/null || true + fi + + echo "" + echo -e "${GREEN}=============================================================================${NC}" + echo -e "${GREEN} RECREACION COMPLETADA EXITOSAMENTE${NC}" + echo -e "${GREEN}=============================================================================${NC}" + echo -e " Schemas: ${YELLOW}$schema_count${NC}" + echo -e " Tablas: ${YELLOW}$table_count${NC}" + [ "$LOAD_SEEDS" = true ] && echo -e " Seeds: ${YELLOW}$ENVIRONMENT${NC}" + echo -e "${GREEN}=============================================================================${NC}" + + if [ "$LOAD_SEEDS" = true ] && [ "$ENVIRONMENT" = "dev" ]; then + echo "" + echo -e "${CYAN}Usuarios demo disponibles:${NC}" + echo " - admin@demo.com / Demo2026! (Super Admin)" + echo " - director@demo.com / Demo2026! (Director)" + echo " - gerente1@demo.com / Demo2026! (Gerente Obra)" + echo " - residente1@demo.com / Demo2026! (Residente)" + echo "" + echo -e "${CYAN}Tenant ID:${NC} 00000000-0000-0000-0003-000000000001" + echo "" + fi +} + +# ============================================================================= +# MAIN +# ============================================================================= + +main() { + parse_args "$@" + + echo -e "${BLUE}=============================================================================${NC}" + echo -e "${BLUE} ERP CONSTRUCCION - Recreacion de Base de Datos${NC}" + echo -e "${BLUE}=============================================================================${NC}" + echo "" + + local db_name=$(echo "$DATABASE_URL" | sed -E 's|.*/([^?]+).*|\1|') + local db_host=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:]+):.*|\1|') + local db_port=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|') + + echo -e "Database: ${YELLOW}$db_name${NC}" + echo -e "Host: ${YELLOW}$db_host:$db_port${NC}" + echo -e "Seeds: ${YELLOW}$([ "$LOAD_SEEDS" = true ] && echo "$ENVIRONMENT" || echo "No")${NC}" + echo "" + + # Ejecutar pasos + verify_connection + confirm_drop + + echo "" + drop_schemas + create_base_schemas + execute_init_ddl + execute_schema_ddl + create_complementary_tables + grant_permissions + + if [ "$LOAD_SEEDS" = true ]; then + echo "" + load_seeds + fi + + echo "" + verify_result +} + +main "$@" diff --git a/fix_auth_tables.sql b/fix_auth_tables.sql new file mode 100644 index 0000000..3b5fcd1 --- /dev/null +++ b/fix_auth_tables.sql @@ -0,0 +1,70 @@ +-- Fix auth tables for erp_construccion_demo + +-- 1. Add missing columns to auth.users +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS roles VARCHAR[] DEFAULT ARRAY['viewer']::VARCHAR[]; +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS default_tenant_id UUID; +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ; +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS username VARCHAR(100); + +-- 2. Add missing columns to auth.tenants +ALTER TABLE auth.tenants ADD COLUMN IF NOT EXISTS code VARCHAR(50) UNIQUE; +ALTER TABLE auth.tenants ADD COLUMN IF NOT EXISTS settings JSONB DEFAULT '{}'; +ALTER TABLE auth.tenants ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +-- Update tenants code from slug +UPDATE auth.tenants SET code = slug WHERE code IS NULL; + +-- 3. Create refresh_tokens table +CREATE TABLE IF NOT EXISTS auth.refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + user_agent VARCHAR(500), + ip_address VARCHAR(45) +); + +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON auth.refresh_tokens(user_id); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON auth.refresh_tokens(token); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON auth.refresh_tokens(expires_at); + +-- 4. Create test tenant if not exists +INSERT INTO auth.tenants (id, name, slug, code, is_active, created_at, updated_at) +VALUES ( + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'Test Company', + 'test-company', + 'test-company', + true, + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + +-- 5. Create admin user with password 'Admin123!' +INSERT INTO auth.users ( + id, tenant_id, email, password_hash, first_name, last_name, + is_active, email_verified, roles, default_tenant_id, created_at, updated_at +) +VALUES ( + 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'admin@test.com', + '$2a$10$4M7jT3YmH14xyx3L7uojmuRFyggZcM/qb8A/54B2XXDtYBmVI7/J2', + 'Admin', + 'User', + true, + true, + ARRAY['admin']::VARCHAR[], + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + NOW(), + NOW() +) ON CONFLICT (id) DO UPDATE SET + password_hash = '$2a$10$4M7jT3YmH14xyx3L7uojmuRFyggZcM/qb8A/54B2XXDtYBmVI7/J2', + roles = ARRAY['admin']::VARCHAR[], + default_tenant_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'; + +-- 6. Verify +SELECT 'Tenant created:', id, name FROM auth.tenants WHERE id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'; +SELECT 'User created:', id, email, roles FROM auth.users WHERE email = 'admin@test.com'; diff --git a/insert_admin_user.sql b/insert_admin_user.sql new file mode 100644 index 0000000..1a8983e --- /dev/null +++ b/insert_admin_user.sql @@ -0,0 +1,21 @@ +INSERT INTO auth.users ( + id, tenant_id, email, password_hash, first_name, last_name, + is_active, email_verified, roles, default_tenant_id, created_at, updated_at +) +VALUES ( + 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + 'admin@test.com', + '$2a$10$4M7jT3YmH14xyx3L7uojmuRFyggZcM/qb8A/54B2XXDtYBmVI7/J2', + 'Admin', + 'User', + true, + true, + ARRAY['admin']::VARCHAR[], + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', + NOW(), + NOW() +) ON CONFLICT (id) DO UPDATE SET + password_hash = EXCLUDED.password_hash, + roles = EXCLUDED.roles, + default_tenant_id = EXCLUDED.default_tenant_id; diff --git a/schemas/01-construction-schema-ddl.sql b/schemas/01-construction-schema-ddl.sql index 13976b4..2b5719a 100644 --- a/schemas/01-construction-schema-ddl.sql +++ b/schemas/01-construction-schema-ddl.sql @@ -75,10 +75,34 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- TABLES - ESTRUCTURA DE PROYECTO -- ============================================================================ +-- Tabla: proyectos (proyectos de desarrollo inmobiliario) +CREATE TABLE IF NOT EXISTS construction.proyectos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + descripcion TEXT, + direccion TEXT, + ciudad VARCHAR(100), + estado VARCHAR(100), + fecha_inicio DATE, + fecha_fin_estimada DATE, + estado_proyecto VARCHAR(20) NOT NULL DEFAULT 'activo', + 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_proyectos_codigo_tenant UNIQUE (tenant_id, codigo), + CONSTRAINT chk_proyectos_estado CHECK (estado_proyecto IN ('activo', 'pausado', 'completado', 'cancelado')) +); + -- 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, + proyecto_id UUID NOT NULL REFERENCES construction.proyectos(id) ON DELETE CASCADE, code VARCHAR(20) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, @@ -739,8 +763,14 @@ CREATE TABLE IF NOT EXISTS construction.contrato_addendas ( -- INDICES -- ============================================================================ +-- Proyectos +CREATE INDEX IF NOT EXISTS idx_proyectos_tenant_id ON construction.proyectos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_proyectos_estado ON construction.proyectos(estado_proyecto); +CREATE INDEX IF NOT EXISTS idx_proyectos_codigo ON construction.proyectos(codigo); + -- Fraccionamientos CREATE INDEX IF NOT EXISTS idx_fraccionamientos_tenant_id ON construction.fraccionamientos(tenant_id); +CREATE INDEX IF NOT EXISTS idx_fraccionamientos_proyecto_id ON construction.fraccionamientos(proyecto_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); @@ -814,6 +844,7 @@ CREATE INDEX IF NOT EXISTS idx_contratos_fraccionamiento_id ON construction.cont -- ROW LEVEL SECURITY (RLS) -- ============================================================================ +ALTER TABLE construction.proyectos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.fraccionamientos ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.etapas ENABLE ROW LEVEL SECURITY; ALTER TABLE construction.manzanas ENABLE ROW LEVEL SECURITY; @@ -840,6 +871,12 @@ 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_proyectos ON construction.proyectos; + CREATE POLICY tenant_isolation_proyectos ON construction.proyectos + 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_fraccionamientos ON construction.fraccionamientos; CREATE POLICY tenant_isolation_fraccionamientos ON construction.fraccionamientos @@ -989,7 +1026,8 @@ EXCEPTION WHEN undefined_object THEN NULL; END $$; -- ============================================================================ 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.proyectos IS 'Proyectos de desarrollo inmobiliario'; +COMMENT ON TABLE construction.fraccionamientos IS 'Fraccionamientos/obras dentro de un proyecto'; 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)'; @@ -1009,5 +1047,5 @@ COMMENT ON TABLE construction.contratos IS 'Contratos con subcontratistas'; -- ============================================================================ -- FIN DEL SCHEMA CONSTRUCTION --- Total tablas: 24 +-- Total tablas: 25 (agregada proyectos) -- ============================================================================ diff --git a/seeds/dev/03-empresa-tenant.sql b/seeds/dev/03-empresa-tenant.sql index 3d5c1b4..166c055 100644 --- a/seeds/dev/03-empresa-tenant.sql +++ b/seeds/dev/03-empresa-tenant.sql @@ -4,7 +4,7 @@ -- Proyecto: ERP Construcción -- Autor: Claude Opus 4.5 -- Fecha: 2026-02-03 --- Descripción: Tenant demo, sucursales, almacenes, configuración +-- Descripción: Tenant demo, sucursales, configuración -- Dependencias: 01-core-catalogs.sql, 02-users-profiles.sql -- ═══════════════════════════════════════════════════════════════════════════════ @@ -14,7 +14,15 @@ BEGIN; -- ───────────────────────────────────────────────────────────────────────────────── --- 1. TENANT (EMPRESA) PRINCIPAL +-- 1. TENANT EN auth.tenants (requerido para FK en otros schemas) +-- ───────────────────────────────────────────────────────────────────────────────── + +INSERT INTO auth.tenants (id, code, name, is_active, created_at) VALUES +('00000000-0000-0000-0003-000000000001', 'DEMO001', 'Constructora Demo', true, NOW()) +ON CONFLICT (id) DO NOTHING; + +-- ───────────────────────────────────────────────────────────────────────────────── +-- 2. TENANT (EMPRESA) PRINCIPAL - Datos extendidos en tenants.tenants -- ───────────────────────────────────────────────────────────────────────────────── INSERT INTO tenants.tenants ( @@ -53,19 +61,19 @@ INSERT INTO tenants.tenants ( ON CONFLICT (id) DO NOTHING; -- ───────────────────────────────────────────────────────────────────────────────── --- 2. CONFIGURACIÓN DEL TENANT +-- 3. CONFIGURACIÓN DEL TENANT -- ───────────────────────────────────────────────────────────────────────────────── INSERT INTO tenants.tenant_settings (id, tenant_id, setting_key, setting_value, setting_type, created_at) VALUES --- General +-- General (setting_value es JSONB, strings deben estar entre comillas dobles) ('00000000-0000-0000-0003-000000000101', '00000000-0000-0000-0003-000000000001', - 'timezone', 'America/Mexico_City', 'string', NOW()), + 'timezone', '"America/Mexico_City"', 'string', NOW()), ('00000000-0000-0000-0003-000000000102', '00000000-0000-0000-0003-000000000001', - 'locale', 'es-MX', 'string', NOW()), + 'locale', '"es-MX"', 'string', NOW()), ('00000000-0000-0000-0003-000000000103', '00000000-0000-0000-0003-000000000001', - 'currency', 'MXN', 'string', NOW()), + 'currency', '"MXN"', 'string', NOW()), ('00000000-0000-0000-0003-000000000104', '00000000-0000-0000-0003-000000000001', - 'date_format', 'DD/MM/YYYY', 'string', NOW()), + 'date_format', '"DD/MM/YYYY"', 'string', NOW()), -- Construcción ('00000000-0000-0000-0003-000000000110', '00000000-0000-0000-0003-000000000001', @@ -97,193 +105,53 @@ INSERT INTO tenants.tenant_settings (id, tenant_id, setting_key, setting_value, ON CONFLICT (id) DO NOTHING; -- ───────────────────────────────────────────────────────────────────────────────── --- 3. SUCURSALES / BRANCHES +-- 4. SUCURSALES / BRANCHES (schema simplificado) -- ───────────────────────────────────────────────────────────────────────────────── INSERT INTO tenants.branches ( - id, tenant_id, code, name, branch_type, - address_street, address_city, address_state, address_zip, - phone, email, - manager_id, - is_headquarters, is_active, created_at + id, tenant_id, code, name, + address, city, state, + phone, + is_main, is_active, created_at ) VALUES -- Matriz ('00000000-0000-0000-0003-000000000201', '00000000-0000-0000-0003-000000000001', - 'MATRIZ', 'Oficinas Corporativas', 'headquarters', - 'Av. Tecnológico 1500, Parque Industrial', 'Aguascalientes', 'Aguascalientes', '20290', - '+52 449 123 4567', 'matriz@constructorademo.com', - '00000000-0000-0000-0002-000000000102', -- director + 'MATRIZ', 'Oficinas Corporativas', + 'Av. Tecnológico 1500, Parque Industrial', 'Aguascalientes', 'Aguascalientes', + '+52 449 123 4567', true, true, NOW()), -- Sucursal Obra Norte ('00000000-0000-0000-0003-000000000202', '00000000-0000-0000-0003-000000000001', - 'OBRA-NORTE', 'Oficina de Obra - Zona Norte', 'field_office', - 'Carr. Panamericana Km 15', 'Aguascalientes', 'Aguascalientes', '20340', - '+52 449 234 5678', 'obranorte@constructorademo.com', - '00000000-0000-0000-0002-000000000103', -- gerente1 + 'OBRA-NORTE', 'Oficina de Obra - Zona Norte', + 'Carr. Panamericana Km 15', 'Aguascalientes', 'Aguascalientes', + '+52 449 234 5678', false, true, NOW()) ON CONFLICT (id) DO NOTHING; --- ───────────────────────────────────────────────────────────────────────────────── --- 4. ALMACENES --- ───────────────────────────────────────────────────────────────────────────────── - -INSERT INTO inventory.warehouses ( - id, tenant_id, branch_id, code, name, warehouse_type, - address_street, address_city, address_state, - manager_id, - is_default, is_active, created_at -) VALUES --- Almacén Central (Matriz) -('00000000-0000-0000-0003-000000000301', '00000000-0000-0000-0003-000000000001', - '00000000-0000-0000-0003-000000000201', - 'ALM-CENTRAL', 'Almacén Central', 'central', - 'Av. Tecnológico 1500', 'Aguascalientes', 'Aguascalientes', - '00000000-0000-0000-0002-000000000106', -- almacenista - true, true, NOW()), - --- Almacén de Obra (Palmas) -('00000000-0000-0000-0003-000000000302', '00000000-0000-0000-0003-000000000001', - '00000000-0000-0000-0003-000000000202', - 'ALM-PALMAS', 'Almacén Residencial Las Palmas', 'field', - 'Dentro del fraccionamiento Las Palmas', 'Aguascalientes', 'Aguascalientes', - NULL, - false, true, NOW()), - --- Almacén de Obra (Diamante) -('00000000-0000-0000-0003-000000000303', '00000000-0000-0000-0003-000000000001', - '00000000-0000-0000-0003-000000000202', - 'ALM-DIAMANTE', 'Almacén Torre Diamante', 'field', - 'Torre Diamante, Zona Centro', 'Aguascalientes', 'Aguascalientes', - NULL, - false, true, NOW()), - --- Almacén de Tránsito -('00000000-0000-0000-0003-000000000304', '00000000-0000-0000-0003-000000000001', - '00000000-0000-0000-0003-000000000201', - 'ALM-TRANSITO', 'Almacén de Tránsito', 'transit', - 'Av. Tecnológico 1500', 'Aguascalientes', 'Aguascalientes', - NULL, - false, true, NOW()) -ON CONFLICT (id) DO NOTHING; - --- ───────────────────────────────────────────────────────────────────────────────── --- 5. ASIGNAR USUARIOS AL TENANT --- ───────────────────────────────────────────────────────────────────────────────── - -INSERT INTO auth.user_tenants (user_id, tenant_id, is_default, is_active, created_at) VALUES -('00000000-0000-0000-0002-000000000101', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000102', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000103', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000104', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000105', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000106', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000107', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000108', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000109', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000110', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000111', '00000000-0000-0000-0003-000000000001', true, true, NOW()), -('00000000-0000-0000-0002-000000000112', '00000000-0000-0000-0003-000000000001', true, true, NOW()) -ON CONFLICT DO NOTHING; - --- ───────────────────────────────────────────────────────────────────────────────── --- 6. PERFILES DE USUARIO (Datos adicionales) --- ───────────────────────────────────────────────────────────────────────────────── - -INSERT INTO profiles.user_profiles ( - id, user_id, tenant_id, - phone, mobile, job_title, department, - avatar_url, bio, - preferences, - created_at -) VALUES -('00000000-0000-0000-0003-000000000401', '00000000-0000-0000-0002-000000000101', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0001', '+52 449 200 0001', 'Administrador del Sistema', 'TI', - '/avatars/admin.png', 'Administrador general del sistema ERP', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000402', '00000000-0000-0000-0002-000000000102', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0002', '+52 449 200 0002', 'Director General', 'Dirección', - '/avatars/director.png', 'Director General de Constructora Demo', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000403', '00000000-0000-0000-0002-000000000103', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0003', '+52 449 200 0003', 'Gerente de Proyectos', 'Construcción', - '/avatars/gerente.png', 'Gerente de proyectos con 15 años de experiencia', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000404', '00000000-0000-0000-0002-000000000104', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0004', '+52 449 200 0004', 'Residente de Obra', 'Construcción', - '/avatars/residente.png', 'Residente de obra certificado CMIC', - '{"theme": "dark", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000405', '00000000-0000-0000-0002-000000000105', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0005', '+52 449 200 0005', 'Supervisor de Campo', 'Construcción', - '/avatars/supervisor.png', 'Supervisor con experiencia en obra residencial', - '{"theme": "dark", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000406', '00000000-0000-0000-0002-000000000106', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0006', '+52 449 200 0006', 'Jefe de Almacén', 'Logística', - '/avatars/almacen.png', 'Responsable de control de inventarios', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000407', '00000000-0000-0000-0002-000000000107', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0007', '+52 449 200 0007', 'Contador General', 'Finanzas', - '/avatars/contador.png', 'CPA con especialidad en construcción', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000408', '00000000-0000-0000-0002-000000000108', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0008', '+52 449 200 0008', 'Coordinador de RRHH', 'Recursos Humanos', - '/avatars/rrhh.png', 'Especialista en gestión de personal de construcción', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000409', '00000000-0000-0000-0002-000000000109', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0009', '+52 449 200 0009', 'Coordinador HSE', 'Seguridad', - '/avatars/hse.png', 'Certificado STPS en seguridad industrial', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000410', '00000000-0000-0000-0002-000000000110', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0010', '+52 449 200 0010', 'Inspector de Calidad', 'Calidad', - '/avatars/calidad.png', 'Inspector certificado ISO 9001', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000411', '00000000-0000-0000-0002-000000000111', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0011', '+52 449 200 0011', 'Comprador', 'Compras', - '/avatars/compras.png', 'Especialista en compras de construcción', - '{"theme": "light", "notifications": true}'::jsonb, NOW()), - -('00000000-0000-0000-0003-000000000412', '00000000-0000-0000-0002-000000000112', '00000000-0000-0000-0003-000000000001', - '+52 449 100 0012', '+52 449 200 0012', 'Cliente', 'Externo', - '/avatars/cliente.png', 'Cliente del fraccionamiento Las Palmas', - '{"theme": "light", "notifications": true}'::jsonb, NOW()) -ON CONFLICT (id) DO NOTHING; - -- ───────────────────────────────────────────────────────────────────────────────── -- VERIFICACIÓN -- ───────────────────────────────────────────────────────────────────────────────── DO $$ DECLARE + v_auth_tenants INTEGER; v_tenants INTEGER; + v_settings INTEGER; v_branches INTEGER; - v_warehouses INTEGER; - v_user_tenants INTEGER; - v_profiles INTEGER; BEGIN + SELECT COUNT(*) INTO v_auth_tenants FROM auth.tenants; SELECT COUNT(*) INTO v_tenants FROM tenants.tenants; + SELECT COUNT(*) INTO v_settings FROM tenants.tenant_settings; SELECT COUNT(*) INTO v_branches FROM tenants.branches; - SELECT COUNT(*) INTO v_warehouses FROM inventory.warehouses; - SELECT COUNT(*) INTO v_user_tenants FROM auth.user_tenants; - SELECT COUNT(*) INTO v_profiles FROM profiles.user_profiles; RAISE NOTICE '══════════════════════════════════════════════════════════════'; RAISE NOTICE 'SEED 03 - EMPRESA Y ESTRUCTURA - COMPLETADO'; RAISE NOTICE '══════════════════════════════════════════════════════════════'; - RAISE NOTICE 'Tenants (empresas): %', v_tenants; + RAISE NOTICE 'Tenants (auth): %', v_auth_tenants; + RAISE NOTICE 'Tenants (extendido): %', v_tenants; + RAISE NOTICE 'Configuraciones: %', v_settings; RAISE NOTICE 'Sucursales: %', v_branches; - RAISE NOTICE 'Almacenes: %', v_warehouses; - RAISE NOTICE 'Usuarios asignados a tenant: %', v_user_tenants; - RAISE NOTICE 'Perfiles de usuario: %', v_profiles; RAISE NOTICE '══════════════════════════════════════════════════════════════'; END $$; diff --git a/seeds/dev/04-proyectos-obras.sql b/seeds/dev/04-proyectos-obras.sql index ac79be9..9cf3428 100644 --- a/seeds/dev/04-proyectos-obras.sql +++ b/seeds/dev/04-proyectos-obras.sql @@ -103,7 +103,7 @@ ON CONFLICT (id) DO NOTHING; INSERT INTO construction.etapas ( id, tenant_id, fraccionamiento_id, code, name, sequence, status, - total_lots, start_date, end_date, + total_lots, start_date, expected_end_date, created_at, created_by ) VALUES -- Las Palmas - 3 etapas diff --git a/temp_update_hash.sql b/temp_update_hash.sql new file mode 100644 index 0000000..609cbe9 --- /dev/null +++ b/temp_update_hash.sql @@ -0,0 +1 @@ +UPDATE auth.users SET password_hash = '$2a$10$4M7jT3YmH14xyx3L7uojmuRFyggZcM/qb8A/54B2XXDtYBmVI7/J2' WHERE email = 'admin@test.com'; diff --git a/update_password.sql b/update_password.sql new file mode 100644 index 0000000..3d547db --- /dev/null +++ b/update_password.sql @@ -0,0 +1 @@ +UPDATE auth.users SET password_hash = '$2a$10$VLsmoNy6WLdlTGuW1WESFeLDEm2aspa52651R8oJfhRfpNkF/70ii' WHERE email = 'admin@test.com';