#!/usr/bin/env bash set -Eeuo pipefail ROOT="${ROOT:-/var/www/dhv2.korexlabs.com}" API_URL="${API_URL:-http://127.0.0.1:3101/api/v3}" DUMMY_UUID="00000000-0000-4000-8000-000000000000" TMP_MIGRATION="/tmp/dhv2-d537-accept-migration-$$.log" trap 'rm -f "$TMP_MIGRATION"' EXIT cd "$ROOT" echo "========== D5.3.7.1 · VERSIONES ==========" [[ "$(node -p "require('./api-v3/package.json').version")" == "0.19.3-7.1" ]] [[ "$(node -p "require('./web-v2/package.json').version")" == "0.19.3-7.1" ]] grep -Fq "API_PHASE = 'D5.3.7.1'" api-v3/src/version.ts echo "API: 0.19.3-7.1" echo "WEB: 0.19.3-7.1" echo echo "========== D5.3.7.1 · DISCO Y DB ==========" FREE_KB="$(df -Pk "$ROOT" | awk 'NR==2 {print $4}')" (( FREE_KB >= 2 * 1024 * 1024 )) [[ "$(docker inspect dhv2-db --format '{{.State.Health.Status}}')" == "healthy" ]] docker compose exec -T db sh -lc 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' echo "Libre: $(( FREE_KB / 1024 / 1024 )) GB · PostgreSQL healthy" echo echo "========== D5.3.7.1 · MIGRACIONES ==========" docker compose --profile tools run --rm migrate npm run migration:show 2>&1 | tee "$TMP_MIGRATION" grep -Fq 'Pending migrations: no' "$TMP_MIGRATION" echo echo "========== D5.3.7.1 · HEALTH ==========" HEALTH="$(curl -fsS "$API_URL/health")" echo "$HEALTH" grep -Fq '"version":"0.19.3-7.1"' <<<"$HEALTH" grep -Fq '"phase":"D5.3.7.1"' <<<"$HEALTH" grep -Fq '"database":"ok"' <<<"$HEALTH" echo echo "========== D5.3.7.1 · RUTAS PROTEGIDAS ==========" CODE="$(curl -sS -o /tmp/dhv2-d537-orgs-$$.txt -w '%{http_code}' "$API_URL/asset-imports/context/organizations")" [[ "$CODE" == "401" ]] CODE="$(curl -sS -o /tmp/dhv2-d537-plan-$$.txt -w '%{http_code}' "$API_URL/asset-imports/$DUMMY_UUID/plan")" [[ "$CODE" == "401" ]] CODE="$(curl -sS -o /tmp/dhv2-d537-apply-$$.txt -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d '{"planHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' "$API_URL/asset-imports/$DUMMY_UUID/apply")" [[ "$CODE" == "401" ]] rm -f /tmp/dhv2-d537-orgs-$$.txt /tmp/dhv2-d537-plan-$$.txt /tmp/dhv2-d537-apply-$$.txt echo "Contexto, plan y aplicación requieren sesión: 401 OK" echo echo "========== D5.3.7.1 · MODELO Y PERMISOS ==========" DB_CHECK="$( cat <<'SQL' | docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -v ON_ERROR_STOP=1 -v app_user="$APP_DB_USER"' SELECT CASE WHEN to_regclass('public.asset_import_plans') IS NOT NULL AND to_regclass('public.asset_import_plan_items') IS NOT NULL THEN 'TABLES_OK' ELSE 'TABLES_BAD' END; SELECT CASE WHEN EXISTS (SELECT 1 FROM permissions WHERE code='asset_imports.apply') THEN 'PERMISSION_OK' ELSE 'PERMISSION_BAD' END; SELECT CASE WHEN NOT EXISTS ( SELECT role.code FROM roles role WHERE role.code IN ('admin','director') AND NOT EXISTS ( SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=role.id AND p.code='asset_imports.apply' ) ) THEN 'ROLE_GRANTS_OK' ELSE 'ROLE_GRANTS_BAD' END; SELECT CASE WHEN has_table_privilege(:'app_user','asset_import_plans','SELECT') AND has_table_privilege(:'app_user','asset_import_plans','INSERT') AND has_table_privilege(:'app_user','asset_import_plans','UPDATE') AND NOT has_table_privilege(:'app_user','asset_import_plans','DELETE') AND has_table_privilege(:'app_user','asset_import_plan_items','SELECT') AND has_table_privilege(:'app_user','asset_import_plan_items','INSERT') AND has_table_privilege(:'app_user','asset_import_plan_items','UPDATE') AND NOT has_table_privilege(:'app_user','asset_import_plan_items','DELETE') THEN 'APP_PRIVILEGES_OK' ELSE 'APP_PRIVILEGES_BAD' END; SELECT CASE WHEN EXISTS (SELECT 1 FROM typeorm_migrations WHERE name='PhaseD537ImportPlanningApplication1787680800000') THEN 'MIGRATION_OK' ELSE 'MIGRATION_BAD' END; SQL )" echo "$DB_CHECK" grep -Fq 'TABLES_OK' <<<"$DB_CHECK" grep -Fq 'PERMISSION_OK' <<<"$DB_CHECK" grep -Fq 'ROLE_GRANTS_OK' <<<"$DB_CHECK" grep -Fq 'APP_PRIVILEGES_OK' <<<"$DB_CHECK" grep -Fq 'MIGRATION_OK' <<<"$DB_CHECK" echo echo "========== D5.3.7.1 · PLAN SEGURO ==========" grep -Fq 'export interface PlanItemRow' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "status === 'CONFLICT' || status === 'WARNING'" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'PLAN_SOURCE_GROUP_REQUIRES_DECISION' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "organizationKind:/^UTE\\b/i" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "operationalAreaEntityKey:explicitlyUnassigned?null:areaItem.entityKey" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "const operatorUnassigned = isExplicitlyUnassignedOperator(operator)" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "operatorUnassigned ? '' : plainImportKey(operator)" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'generatedImportCode(plan.id, item.assetTypeCode, item.entityKey)' api-v3/src/asset-imports/asset-imports.service.ts echo "Warnings bloqueantes, entidades únicas, UTE, operador no asignado y jerarquía controlada: OK" echo echo "========== D5.3.7.1 · APLICACIÓN TRANSACCIONAL ==========" grep -Fq 'async applyPlan' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'this.dataSource.transaction' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'ASSET_IMPORT_PLAN_STALE' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'ASSET_IMPORT_PARENT_RULE_INVALID' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'ASSET_IMPORT_OPERATIONAL_CONTEXT_INCOMPLETE' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'const areaKey=String(item.payload.operationalAreaEntityKey' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'PLAN_MATCH_OPERATIONAL_CONTEXT_MISMATCH' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'revalidateMatchedOperationalContext' api-v3/src/asset-imports/asset-imports.service.ts grep -Fq "permission: 'asset_imports.apply'" web-v2/src 2>/dev/null || grep -Fq "asset_imports.apply" web-v2/src/pages/AssetImportsPage.tsx echo "Hash, transacción, jerarquía, contexto operativo y permiso de aplicación: OK" echo echo "========== D5.3.7.1 · REVERSIÓN LÓGICA ==========" grep -Fq "information_status='INACTIVE'" api-v3/src/asset-imports/asset-imports.service.ts grep -Fq 'imported_asset_id=NULL' api-v3/src/asset-imports/asset-imports.service.ts if grep -Fq 'DELETE FROM assets' api-v3/src/asset-imports/asset-imports.service.ts; then echo "ERROR: la reversión no debe borrar assets"; exit 1 fi echo "Rollback lógico, trazable y sin DELETE de activos: OK" echo echo "========== D5.3.7.1 · FRONTEND ==========" grep -Fq '4 · Plan' web-v2/src/pages/AssetImportsPage.tsx grep -Fq '5 · Confirmación' web-v2/src/pages/AssetImportsPage.tsx grep -Fq '6 · Aplicación' web-v2/src/pages/AssetImportsPage.tsx grep -Fq 'APLICAR' web-v2/src/pages/AssetImportsPage.tsx grep -Fq 'operatorAssetId' web-v2/src/pages/AssetImportsPage.tsx grep -Fq 'Revertir lote' web-v2/src/pages/AssetImportsPage.tsx echo "Plan, confirmación, operador obligatorio y reversión visibles: OK" echo echo "========== D5.3.7.1 · CONTENEDORES ==========" docker compose ps -a [[ "$(docker inspect dhv2-db --format '{{.State.Health.Status}}')" == "healthy" ]] [[ "$(docker inspect dhv2-api --format '{{.State.Status}}')" == "running" ]] [[ "$(docker inspect dhv2-web --format '{{.State.Status}}')" == "running" ]] echo echo "ACEPTACIÓN DE FASE D5.3.7.1: OK"