From 844c1a950fe6e37c01fc6c81e5f7a0c0cf681d6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:06:16 +0000 Subject: [PATCH] fix: close F4 pre-merge review gaps --- .../act-administration.service.ts | 4 +-- .../field-briefing.service.ts | 2 +- ...f4-align-document-lifecycle-constraints.ts | 8 +++++ .../inspection-closing.service.ts | 9 +---- .../unit/f4-premerge-review-contract.test.ts | 35 +++++++++++++++++++ web-v2/src/pages/ActAdministrationPage.tsx | 2 +- 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 api-v3/test/unit/f4-premerge-review-contract.test.ts diff --git a/api-v3/src/act-administration/act-administration.service.ts b/api-v3/src/act-administration/act-administration.service.ts index c582b48..38f4e2d 100644 --- a/api-v3/src/act-administration/act-administration.service.ts +++ b/api-v3/src/act-administration/act-administration.service.ts @@ -49,7 +49,7 @@ export class ActAdministrationService { COUNT(*) FILTER (WHERE f.status = 'OPEN' AND f.next_control_on IS NOT NULL) AS scheduled_control_count FROM inspection_findings f WHERE f.act_id = ia.id ) fc ON true - WHERE ia.status IN ('CLOSED', 'RECTIFIED') + WHERE ia.status IN ('SEALED', 'CLOSED', 'RECTIFIED') ), classified AS ( SELECT *, CASE WHEN "findingCount" > 0 AND "openFindingCount" = 0 THEN 'REGULARIZED' @@ -110,7 +110,7 @@ export class ActAdministrationService { const rows = await this.dataSource.query(`SELECT id, code, status FROM inspection_acts WHERE id = $1`, [actId]); const act = rows[0]; if (!act) throw new NotFoundException({ code: 'ACT_NOT_FOUND', message: 'Acta inexistente.' }); - 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.' }); return act; } diff --git a/api-v3/src/act-administration/field-briefing.service.ts b/api-v3/src/act-administration/field-briefing.service.ts index c718ca8..5b7f917 100644 --- a/api-v3/src/act-administration/field-briefing.service.ts +++ b/api-v3/src/act-administration/field-briefing.service.ts @@ -129,7 +129,7 @@ export class FieldBriefingService { ORDER BY company_response.received_on DESC, company_response.created_at DESC, company_response.id DESC LIMIT 1 ) response ON true - WHERE act.status IN ('CLOSED', 'RECTIFIED') + WHERE act.status IN ('SEALED', 'CLOSED', 'RECTIFIED') AND source_visit.id <> $1 AND source_visit.operational_area_id = $2::uuid AND source_visit.operator_company_id = $3::uuid diff --git a/api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts b/api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts index 6171183..2a6a81b 100644 --- a/api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts +++ b/api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts @@ -22,6 +22,10 @@ export class F4AlignDocumentLifecycleConstraints1790000600000 implements Migrati status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED') ) `); + await queryRunner.query(` + ALTER TABLE inspection_reports + ALTER COLUMN status SET DEFAULT 'WORKING' + `); await queryRunner.query(` ALTER TABLE inspection_acts @@ -163,6 +167,10 @@ export class F4AlignDocumentLifecycleConstraints1790000600000 implements Migrati SET status='FROZEN' WHERE status IN ('WORKING','OFFICIALIZED') `); + await queryRunner.query(` + ALTER TABLE inspection_reports + ALTER COLUMN status SET DEFAULT 'FROZEN' + `); await queryRunner.query(` ALTER TABLE inspection_reports DROP CONSTRAINT IF EXISTS chk_inspection_reports_status diff --git a/api-v3/src/inspection-closing/inspection-closing.service.ts b/api-v3/src/inspection-closing/inspection-closing.service.ts index 253a4e4..be1c906 100644 --- a/api-v3/src/inspection-closing/inspection-closing.service.ts +++ b/api-v3/src/inspection-closing/inspection-closing.service.ts @@ -549,14 +549,7 @@ export class InspectionClosingService { if (companyOutcomes.length !== 1) { throw new ConflictException({ code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED', - message: 'Debe existir exactamente una firma, disidencia o negativa del responsable de la empresa', - }); - } - const companyOutcome = companyOutcomes[0]; - if (companyOutcome.status === InspectionActSignatureStatus.ABSENT) { - throw new ConflictException({ - code: 'INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING', - message: 'La ausencia no reemplaza la firma o negativa; la manifestación de la empresa sigue pendiente', + message: 'Debe existir exactamente una firma, disidencia, negativa o ausencia documentada del responsable de la empresa', }); } diff --git a/api-v3/test/unit/f4-premerge-review-contract.test.ts b/api-v3/test/unit/f4-premerge-review-contract.test.ts new file mode 100644 index 0000000..a6a5700 --- /dev/null +++ b/api-v3/test/unit/f4-premerge-review-contract.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; + +const root = process.cwd(); +const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8'); + +const migration = read('src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts'); +const closing = read('src/inspection-closing/inspection-closing.service.ts'); +const administration = read('src/act-administration/act-administration.service.ts'); +const briefing = read('src/act-administration/field-briefing.service.ts'); + +test('F4 report status default is WORKING physically and rollback restores FROZEN', () => { + const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down(')); + const down = migration.slice(migration.indexOf(' public async down(')); + assert.match(up, /ALTER COLUMN status SET DEFAULT 'WORKING'/); + assert.match(down, /ALTER COLUMN status SET DEFAULT 'FROZEN'/); +}); + +test('F4 allows a documented company absence to satisfy the terminal manifestation required for sealing', () => { + const closeStart = closing.indexOf(' async close('); + const closeEnd = closing.indexOf(' async signatureContent(', closeStart); + const close = closing.slice(closeStart, closeEnd); + assert.match(close, /companyOutcomes\.length !== 1/); + assert.match(close, /ausencia documentada/); + assert.doesNotMatch(close, /INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING/); + assert.doesNotMatch(close, /companyOutcome\.status === InspectionActSignatureStatus\.ABSENT/); +}); + +test('F4 SEALED Acts remain visible in administrative follow-up and field briefing', () => { + assert.match(administration, /WHERE ia\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/); + assert.match(administration, /\['SEALED', 'CLOSED', 'RECTIFIED'\]\.includes\(act\.status\)/); + assert.match(briefing, /WHERE act\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/); +}); diff --git a/web-v2/src/pages/ActAdministrationPage.tsx b/web-v2/src/pages/ActAdministrationPage.tsx index aadfe6e..6a860b5 100644 --- a/web-v2/src/pages/ActAdministrationPage.tsx +++ b/web-v2/src/pages/ActAdministrationPage.tsx @@ -16,7 +16,7 @@ const label=(s:ActAdministrationState)=>states.find(x=>x.value===s)?.label??s; export function ActAdministrationPage(){ const ctx=useOperationalContext(); const [state,setState]=useState<'ALL'|ActAdministrationState>('ALL'); const [items,setItems]=useState([]); const [meta,setMeta]=useState({page:1,pageSize:25,total:0,totalPages:0}); const [counters,setCounters]=useState>>({}); const [loading,setLoading]=useState(true); const [error,setError]=useState(''); useEffect(()=>{setLoading(true);setError('');listActAdministrationQueue({state,areaId:ctx.areaId||undefined,companyId:ctx.companyId||undefined,page:1,pageSize:50}).then(r=>{setItems(r.data);setMeta(r.meta);setCounters(r.counters)}).catch(e=>setError(errorMessage(e))).finally(()=>setLoading(false));},[state,ctx.areaId,ctx.companyId]); - return
SEGUIMIENTO ADMINISTRATIVO

Seguimiento de Actas

El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. Los hallazgos quedan dentro de su expediente.

+ return
SEGUIMIENTO ADMINISTRATIVO

Seguimiento de Actas

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.

{states.map(s=>)}
{error&&{error}}{loading?:items.length===0?:
{meta.total} acta{meta.total===1?'':'s'}
{items.map(a=>)}
ActaÁrea / empresaHallazgosPlazo empresaRespuestaEstado
{a.actCode}{formatDate(a.occurredAt)}{a.areaName??'—'}{a.companyName??'—'}{a.openFindingCount} abiertos / {a.findingCount}{a.responseDueOn?formatDate(a.responseDueOn):'Sin definir'}{a.responseReceivedOn?formatDate(a.responseReceivedOn):'Pendiente'}{label(a.adminState)}Gestionar
}
; }