#!/usr/bin/env bash set -Eeuo pipefail dhv2_base_url="${DHV2_BASE_URL:-https://dhv2.korexlabs.com}" dhv2_base_url="${dhv2_base_url%/}" dhv2_nil_uuid="00000000-0000-4000-8000-000000000000" fail() { echo "ERROR: $*" >&2 exit 1 } echo "========== D5 · MIGRACIONES ==========" dhv2_migration_status="$(docker compose --profile tools run --rm migrate npm run migration:show 2>&1)" grep -q 'Pending migrations: no' <<<"$dhv2_migration_status" \ || fail "Quedaron migraciones pendientes" echo "Migraciones pendientes: no" echo echo "========== D5 · HEALTH ==========" dhv2_internal="$(curl -fsS --connect-timeout 10 --max-time 30 http://127.0.0.1:3101/api/v3/health)" dhv2_external="$(curl -fsS --connect-timeout 10 --max-time 30 "$dhv2_base_url/api/v3/health")" grep -q '"version":"0.19.0"' <<<"$dhv2_internal" || fail "Health interno no informa v0.19.0" grep -q '"phase":"D5"' <<<"$dhv2_internal" || fail "Health interno no informa D5" grep -q '"version":"0.19.0"' <<<"$dhv2_external" || fail "Health externo no informa v0.19.0" grep -q '"database":"ok"' <<<"$dhv2_external" || fail "La base no está saludable" echo "$dhv2_external" echo echo "========== D5 · RUTAS PROTEGIDAS ==========" dhv2_get_paths=( "inspection-acts/$dhv2_nil_uuid/closure" "inspection-act-signatures/$dhv2_nil_uuid/content" ) for dhv2_path in "${dhv2_get_paths[@]}"; do dhv2_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -o /tmp/dhv2-d5-response.json -w '%{http_code}' \ "$dhv2_base_url/api/v3/$dhv2_path")" test "$dhv2_code" = "401" \ || fail "/api/v3/$dhv2_path devolvió HTTP $dhv2_code; se esperaba 401" echo "/api/v3/$dhv2_path sin sesión: 401 OK" done dhv2_post_paths=( "inspection-acts/$dhv2_nil_uuid/ready" "inspection-acts/$dhv2_nil_uuid/company-outcome" "inspection-acts/$dhv2_nil_uuid/close" ) for dhv2_path in "${dhv2_post_paths[@]}"; do dhv2_code="$(curl -sS --connect-timeout 10 --max-time 30 \ -X POST -H 'Content-Type: application/json' -d '{}' \ -o /tmp/dhv2-d5-response.json -w '%{http_code}' \ "$dhv2_base_url/api/v3/$dhv2_path")" test "$dhv2_code" = "401" \ || fail "/api/v3/$dhv2_path devolvió HTTP $dhv2_code; se esperaba 401" echo "/api/v3/$dhv2_path sin sesión: 401 OK" done rm -f /tmp/dhv2-d5-response.json echo echo "========== D5 · MODELO E INTEGRIDAD ==========" dhv2_db_check="$(docker compose exec -T db sh -lc \ 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -At' <<'SQL' SELECT 'tables', COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ( 'inspection_act_responsibles', 'inspection_act_closures', 'inspection_act_signatures' ); SELECT 'act_closure_columns', COUNT(*) FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'inspection_acts' AND column_name IN ('closed_at', 'closed_by', 'closure_sha256'); SELECT 'responsible_checks', COUNT(*) FROM pg_constraint WHERE conname IN ( 'chk_inspection_act_responsibles_attendance', 'chk_inspection_act_responsibles_document_type', 'chk_inspection_act_responsibles_details' ); SELECT 'closure_checks', COUNT(*) FROM pg_constraint WHERE conname IN ( 'chk_inspection_act_closures_schema', 'chk_inspection_act_closures_prepared_sha', 'chk_inspection_act_closures_final_sha', 'chk_inspection_act_closures_upload_mode', 'chk_inspection_act_closures_final' ); SELECT 'signature_checks', COUNT(*) FROM pg_constraint WHERE conname IN ( 'chk_inspection_act_signatures_signer_type', 'chk_inspection_act_signatures_status', 'chk_inspection_act_signatures_document_type', 'chk_inspection_act_signatures_source', 'chk_inspection_act_signatures_sha', 'chk_inspection_act_signatures_coordinates', 'chk_inspection_act_signatures_accuracy', 'chk_inspection_act_signatures_inspector', 'chk_inspection_act_signatures_company', 'chk_inspection_act_signatures_signed' ); SELECT 'unique_guards', COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND indexname IN ( 'uq_inspection_act_inspector_signature', 'uq_inspection_act_company_outcome', 'uq_inspection_act_signatures_stored_name', 'uq_inspection_act_closures_final_sha' ); SELECT 'integrity_triggers', COUNT(*) FROM pg_trigger WHERE NOT tgisinternal AND tgname IN ( 'trg_inspection_act_responsibles_draft', 'trg_inspection_act_signatures_ready', 'trg_inspection_acts_closed_immutable', 'trg_inspection_act_closures_immutable', 'trg_inspection_act_assets_draft', 'trg_inspection_findings_frozen_fields', 'trg_inspection_evidence_observation_draft' ); SELECT 'permissions', COUNT(*) FROM permissions WHERE code IN ( 'inspection_closure.read', 'inspection_closure.prepare', 'inspection_closure.sign', 'inspection_closure.close' ); SELECT 'read_roles', COUNT(*) FROM role_permissions rp INNER JOIN roles role ON role.id = rp.role_id INNER JOIN permissions permission ON permission.id = rp.permission_id WHERE role.code IN ('admin', 'director', 'supervisor', 'inspector', 'auditor') AND permission.code = 'inspection_closure.read'; SELECT 'prepare_roles', COUNT(*) FROM role_permissions rp INNER JOIN roles role ON role.id = rp.role_id INNER JOIN permissions permission ON permission.id = rp.permission_id WHERE role.code IN ('admin', 'supervisor', 'inspector') AND permission.code = 'inspection_closure.prepare'; SELECT 'sign_roles', COUNT(*) FROM role_permissions rp INNER JOIN roles role ON role.id = rp.role_id INNER JOIN permissions permission ON permission.id = rp.permission_id WHERE role.code IN ('admin', 'supervisor', 'inspector') AND permission.code = 'inspection_closure.sign'; SELECT 'close_roles', COUNT(*) FROM role_permissions rp INNER JOIN roles role ON role.id = rp.role_id INNER JOIN permissions permission ON permission.id = rp.permission_id WHERE role.code IN ('admin', 'director', 'supervisor', 'inspector') AND permission.code = 'inspection_closure.close'; SQL )" grep -q 'tables|3' <<<"$dhv2_db_check" || fail "Faltan tablas D5" grep -q 'act_closure_columns|3' <<<"$dhv2_db_check" || fail "Faltan columnas de cierre en el acta" grep -q 'responsible_checks|3' <<<"$dhv2_db_check" || fail "Faltan controles del responsable" grep -q 'closure_checks|5' <<<"$dhv2_db_check" || fail "Faltan controles del cierre" grep -q 'signature_checks|10' <<<"$dhv2_db_check" || fail "Faltan controles de firmas" grep -q 'unique_guards|4' <<<"$dhv2_db_check" || fail "Faltan claves únicas D5" grep -q 'integrity_triggers|7' <<<"$dhv2_db_check" || fail "Faltan triggers de integridad D5" grep -q 'permissions|4' <<<"$dhv2_db_check" || fail "Faltan permisos D5" grep -q 'read_roles|5' <<<"$dhv2_db_check" || fail "Faltan roles de lectura" grep -q 'prepare_roles|3' <<<"$dhv2_db_check" || fail "Faltan roles de preparación" grep -q 'sign_roles|3' <<<"$dhv2_db_check" || fail "Faltan roles de firma" grep -q 'close_roles|4' <<<"$dhv2_db_check" || fail "Faltan roles de cierre" echo "$dhv2_db_check" echo echo "========== D5 · PRIVILEGIOS RUNTIME ==========" dhv2_privileges="$(docker compose exec -T db sh -lc \ 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v app_user="$APP_DB_USER" -At' <<'SQL' SELECT 'responsible_select', has_table_privilege(:'app_user', 'inspection_act_responsibles', 'SELECT'); SELECT 'responsible_insert', has_table_privilege(:'app_user', 'inspection_act_responsibles', 'INSERT'); SELECT 'responsible_update', has_table_privilege(:'app_user', 'inspection_act_responsibles', 'UPDATE'); SELECT 'responsible_delete', has_table_privilege(:'app_user', 'inspection_act_responsibles', 'DELETE'); SELECT 'closure_select', has_table_privilege(:'app_user', 'inspection_act_closures', 'SELECT'); SELECT 'closure_insert', has_table_privilege(:'app_user', 'inspection_act_closures', 'INSERT'); SELECT 'closure_update', has_table_privilege(:'app_user', 'inspection_act_closures', 'UPDATE'); SELECT 'closure_delete', has_table_privilege(:'app_user', 'inspection_act_closures', 'DELETE'); SELECT 'signature_select', has_table_privilege(:'app_user', 'inspection_act_signatures', 'SELECT'); SELECT 'signature_insert', has_table_privilege(:'app_user', 'inspection_act_signatures', 'INSERT'); SELECT 'signature_update', has_table_privilege(:'app_user', 'inspection_act_signatures', 'UPDATE'); SELECT 'signature_delete', has_table_privilege(:'app_user', 'inspection_act_signatures', 'DELETE'); SQL )" for dhv2_expected in \ responsible_select responsible_insert responsible_update \ closure_select closure_insert closure_update \ signature_select signature_insert; do grep -q "${dhv2_expected}|t" <<<"$dhv2_privileges" \ || fail "Falta privilegio runtime ${dhv2_expected}" done for dhv2_forbidden in \ responsible_delete closure_delete signature_update signature_delete; do grep -q "${dhv2_forbidden}|f" <<<"$dhv2_privileges" \ || fail "Privilegio runtime indebido ${dhv2_forbidden}" done echo "$dhv2_privileges" echo echo "========== D5 · ALMACENAMIENTO ==========" docker compose exec -T api sh -lc \ 'mkdir -p /app/storage/asset-media/inspection-signatures && chmod 700 /app/storage/asset-media/inspection-signatures && test -w /app/storage/asset-media/inspection-signatures' echo "Directorio persistente de firmas protegido y escribible: OK" echo echo "========== D5 · FRONTEND ==========" docker compose exec -T web sh -lc \ 'grep -R "0.19.0" /usr/share/nginx/html/assets >/dev/null && grep -R "Cierre y firmas" /usr/share/nginx/html/assets >/dev/null && grep -R "Cerrar y sellar" /usr/share/nginx/html/assets >/dev/null && grep -R "firma manuscrita" /usr/share/nginx/html/assets >/dev/null && grep -R "Los hallazgos" /usr/share/nginx/html/assets >/dev/null' echo "Flujo de cierre, firma y footer v0.19.0: OK" echo echo "========== D5 · CONTROLADOR ==========" docker compose logs api | grep 'InspectionClosingController' >/dev/null \ || fail "InspectionClosingController no aparece en las rutas cargadas" echo "InspectionClosingController cargado: OK" echo echo "ACEPTACIÓN DE FASE D5: OK"