fix(f4): decouple GEDO officialization from notification deadline

This commit is contained in:
2026-09-08 07:49:29 -03:00
parent 5386ccf9b5
commit a9b2ed4b4b
@@ -5,7 +5,6 @@ import {
BadRequestException,
ConflictException,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@@ -15,9 +14,6 @@ import { AuditService } from '../audit/audit.service';
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
import {
AuditAction,
InspectionActUrgency,
InspectionDeadlineBasis,
InspectionDeadlineDayType,
InspectionReportStatus,
} from '../database/entities';
import type { CreateInspectionReportFollowUpDto } from './dto/create-inspection-report-follow-up.dto';
@@ -169,7 +165,9 @@ export class InspectionReportWorkflowService {
file!.buffer.length,
sha256,
]);
await this.activateNonUrgentDeadline(manager, report.actId, officializedAt);
// GEDO oficializa el INF, pero no equivale por sí solo a la notificación
// administrativa de un Acta no urgente. El vencimiento queda pendiente
// hasta que el procedimiento defina y registre el evento de notificación.
await this.audit.record({
...administrationAuditContext(principal, request),
action: AuditAction.INSPECTION_REPORT_OFFICIALIZED,
@@ -182,7 +180,12 @@ export class InspectionReportWorkflowService {
gedoPdfOriginalName: file!.originalname,
gedoPdfSha256: sha256,
},
metadata: { reportCode: report.code, actId: report.actId, immutable: true },
metadata: {
reportCode: report.code,
actId: report.actId,
immutable: true,
deadlineActivation: 'PENDING_NOTIFICATION_EVENT',
},
}, manager);
return this.getWorkflowView(manager, reportId);
});
@@ -288,84 +291,6 @@ export class InspectionReportWorkflowService {
`, [reportId]);
}
private async activateNonUrgentDeadline(
manager: EntityManager,
actId: string,
officializedAt: Date,
): Promise<void> {
const [act] = await manager.query(`
SELECT urgency,deadline_days AS "deadlineDays",deadline_day_type AS "deadlineDayType",
deadline_basis AS "deadlineBasis",deadline_at AS "deadlineAt"
FROM inspection_acts WHERE id=$1 FOR UPDATE
`, [actId]) as Array<{
urgency: InspectionActUrgency;
deadlineDays: number | null;
deadlineDayType: InspectionDeadlineDayType | null;
deadlineBasis: InspectionDeadlineBasis | null;
deadlineAt: Date | null;
}>;
if (!act) return;
if (act.urgency !== InspectionActUrgency.NON_URGENT) return;
if (act.deadlineBasis !== InspectionDeadlineBasis.GEDO_DATE) {
throw new ConflictException({
code: 'INVALID_NON_URGENT_DEADLINE_BASIS',
message: 'El acta no urgente no conserva la regla GEDO necesaria para calcular su vencimiento',
});
}
if (!act.deadlineDays || !act.deadlineDayType) {
throw new ConflictException({
code: 'MISSING_ACT_DEADLINE_SNAPSHOT',
message: 'El acta no conserva la política de plazo aplicada al momento de finalizarse',
});
}
const deadlineAt = await this.calculateDeadline(
manager,
officializedAt,
Number(act.deadlineDays),
act.deadlineDayType,
);
await manager.query(`
UPDATE inspection_acts
SET deadline_base_at=$2,deadline_at=$3,updated_at=CURRENT_TIMESTAMP
WHERE id=$1
`, [actId, officializedAt, deadlineAt]);
}
private async calculateDeadline(
manager: EntityManager,
baseAt: Date,
days: number,
dayType: InspectionDeadlineDayType,
): Promise<Date> {
if (dayType === InspectionDeadlineDayType.CALENDAR) {
const [row] = await manager.query(`
SELECT (
(($1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza')::date + $2::integer)::timestamp
+ time '23:59:59'
) AT TIME ZONE 'America/Argentina/Mendoza' AS due_at
`, [baseAt, days]) as Array<{ due_at: Date }>;
return row.due_at;
}
const [row] = await manager.query(`
WITH candidates AS (
SELECT day::date AS day,
COALESCE(override.is_business_day, EXTRACT(ISODOW FROM day)::integer BETWEEN 1 AND 5) AS business
FROM generate_series(
(($1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza')::date + 1)::timestamp,
(($1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza')::date + 730)::timestamp,
interval '1 day'
) day
LEFT JOIN inspection_business_calendar_days override ON override.date=day::date
), ranked AS (
SELECT day,ROW_NUMBER() OVER (ORDER BY day) AS position FROM candidates WHERE business=true
)
SELECT ((day::timestamp + time '23:59:59') AT TIME ZONE 'America/Argentina/Mendoza') AS due_at
FROM ranked WHERE position=$2 LIMIT 1
`, [baseAt, days]) as Array<{ due_at: Date }>;
if (!row?.due_at) throw new InternalServerErrorException('No se pudo calcular el vencimiento');
return row.due_at;
}
private assertPdf(file: UploadedInspectionReportFile | undefined): void {
if (!file?.buffer?.length) {
throw new BadRequestException({