diff --git a/.github/workflows/f4-one-shot-premerge-fix.yml b/.github/workflows/f4-one-shot-premerge-fix.yml new file mode 100644 index 0000000..884f59f --- /dev/null +++ b/.github/workflows/f4-one-shot-premerge-fix.yml @@ -0,0 +1,107 @@ +name: F4 one-shot pre-merge corrections + +on: + push: + branches: + - feature/f4-backend-saneamiento-documental + +permissions: + contents: write + +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: feature/f4-backend-saneamiento-documental + + - name: Apply audited F4 corrections + run: | + python - <<'PY' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise SystemExit(f"{path}: expected exactly one match, got {count}") + file.write_text(text.replace(old, new)) + + # 1) Physical DB default must match the active F4 report lifecycle. + migration = 'api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts' + replace_once( + migration, + """ await queryRunner.query(`\n ALTER TABLE inspection_reports\n ADD CONSTRAINT chk_inspection_reports_status CHECK (\n status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED')\n )\n `);\n\n await queryRunner.query(`\n ALTER TABLE inspection_acts\n""", + """ await queryRunner.query(`\n ALTER TABLE inspection_reports\n ADD CONSTRAINT chk_inspection_reports_status CHECK (\n status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED')\n )\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n ALTER COLUMN status SET DEFAULT 'WORKING'\n `);\n\n await queryRunner.query(`\n ALTER TABLE inspection_acts\n""", + ) + replace_once( + migration, + """ await queryRunner.query(`\n UPDATE inspection_reports\n SET status='FROZEN'\n WHERE status IN ('WORKING','OFFICIALIZED')\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n DROP CONSTRAINT IF EXISTS chk_inspection_reports_status\n""", + """ await queryRunner.query(`\n UPDATE inspection_reports\n SET status='FROZEN'\n WHERE status IN ('WORKING','OFFICIALIZED')\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n ALTER COLUMN status SET DEFAULT 'FROZEN'\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n DROP CONSTRAINT IF EXISTS chk_inspection_reports_status\n""", + ) + + # 2) A documented company absence is a terminal manifestation: it must not strand a LOCKED Act. + closing = 'api-v3/src/inspection-closing/inspection-closing.service.ts' + replace_once( + closing, + """ const companyOutcomes = signatures.filter((item) =>\n item.signerType === InspectionActSignerType.COMPANY_RESPONSIBLE,\n );\n if (companyOutcomes.length !== 1) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED',\n message: 'Debe existir exactamente una firma, disidencia o negativa del responsable de la empresa',\n });\n }\n const companyOutcome = companyOutcomes[0];\n if (companyOutcome.status === InspectionActSignatureStatus.ABSENT) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING',\n message: 'La ausencia no reemplaza la firma o negativa; la manifestación de la empresa sigue pendiente',\n });\n }\n\n const serverSealedAt = new Date();\n""", + """ const companyOutcomes = signatures.filter((item) =>\n item.signerType === InspectionActSignerType.COMPANY_RESPONSIBLE,\n );\n if (companyOutcomes.length !== 1) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED',\n message: 'Debe existir exactamente una firma, disidencia, negativa o ausencia documentada del responsable de la empresa',\n });\n }\n\n const serverSealedAt = new Date();\n""", + ) + + # 3) Legacy administrative follow-up remains available, but F4 SEALED Acts must be visible. + admin = 'api-v3/src/act-administration/act-administration.service.ts' + replace_once( + admin, + "WHERE ia.status IN ('CLOSED', 'RECTIFIED')", + "WHERE ia.status IN ('SEALED', 'CLOSED', 'RECTIFIED')", + ) + replace_once( + admin, + "if (!['CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_CLOSED', message: 'El seguimiento administrativo comienza cuando el Acta está cerrada.' });", + "if (!['SEALED', 'CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_SEALED', message: 'El seguimiento administrativo comienza cuando el Acta está sellada.' });", + ) + + briefing = 'api-v3/src/act-administration/field-briefing.service.ts' + replace_once( + briefing, + "WHERE act.status IN ('CLOSED', 'RECTIFIED')", + "WHERE act.status IN ('SEALED', 'CLOSED', 'RECTIFIED')", + ) + + page = 'web-v2/src/pages/ActAdministrationPage.tsx' + replace_once( + page, + "El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. Los hallazgos quedan dentro de su expediente.", + "El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. GEDO no activa el vencimiento por sí solo; el plazo se define cuando corresponda. Los hallazgos quedan dentro de su expediente.", + ) + + # 4) Regression contracts for the three audited pre-merge gaps. + test_path = Path('api-v3/test/unit/f4-premerge-review-contract.test.ts') + if test_path.exists(): + raise SystemExit(f'{test_path}: file already exists') + test_path.write_text("""import assert from 'node:assert/strict';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport test from 'node:test';\n\nconst root = process.cwd();\nconst read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');\n\nconst migration = read('src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts');\nconst closing = read('src/inspection-closing/inspection-closing.service.ts');\nconst administration = read('src/act-administration/act-administration.service.ts');\nconst briefing = read('src/act-administration/field-briefing.service.ts');\n\ntest('F4 report status default is WORKING physically and rollback restores FROZEN', () => {\n const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down('));\n const down = migration.slice(migration.indexOf(' public async down('));\n assert.match(up, /ALTER COLUMN status SET DEFAULT 'WORKING'/);\n assert.match(down, /ALTER COLUMN status SET DEFAULT 'FROZEN'/);\n});\n\ntest('F4 allows a documented company absence to satisfy the terminal manifestation required for sealing', () => {\n const closeStart = closing.indexOf(' async close(');\n const closeEnd = closing.indexOf(' async signatureContent(', closeStart);\n const close = closing.slice(closeStart, closeEnd);\n assert.match(close, /companyOutcomes\.length !== 1/);\n assert.match(close, /ausencia documentada/);\n assert.doesNotMatch(close, /INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING/);\n assert.doesNotMatch(close, /companyOutcome\.status === InspectionActSignatureStatus\.ABSENT/);\n});\n\ntest('F4 SEALED Acts remain visible in administrative follow-up and field briefing', () => {\n assert.match(administration, /WHERE ia\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);\n assert.match(administration, /\['SEALED', 'CLOSED', 'RECTIFIED'\]\.includes\(act\.status\)/);\n assert.match(briefing, /WHERE act\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);\n});\n""") + PY + + git diff --check + + - name: Commit audited corrections + run: | + set -Eeuo pipefail + if git diff --quiet; then + echo 'No changes produced; refusing silent success.' + exit 1 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts \ + api-v3/src/inspection-closing/inspection-closing.service.ts \ + api-v3/src/act-administration/act-administration.service.ts \ + api-v3/src/act-administration/field-briefing.service.ts \ + api-v3/test/unit/f4-premerge-review-contract.test.ts \ + web-v2/src/pages/ActAdministrationPage.tsx + git commit -m 'fix: close F4 pre-merge review gaps' + git push origin HEAD:feature/f4-backend-saneamiento-documental