fix: close final F4 review gaps

This commit is contained in:
github-actions[bot]
2026-09-08 18:22:02 +00:00
parent 969ffddc76
commit 5dc7ea037d
9 changed files with 182 additions and 137 deletions
@@ -1,10 +1,11 @@
import { createHash, randomUUID } from 'node:crypto';
import { mkdir, unlink, writeFile } from 'node:fs/promises';
import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
import { isAbsolute, parse, resolve } from 'node:path';
import {
BadRequestException,
ConflictException,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@@ -58,7 +59,7 @@ export class InspectionReportWorkflowService {
config: ConfigService,
) {
const configured = config.get<string>('INSPECTION_REPORT_UPLOAD_ROOT')
?? '/app/storage/inspection-reports';
?? '/app/storage/asset-media/inspection-reports';
if (!isAbsolute(configured)) throw new Error('INSPECTION_REPORT_UPLOAD_ROOT must be absolute');
this.root = resolve(configured);
if (this.root === parse(this.root).root) {
@@ -97,6 +98,30 @@ export class InspectionReportWorkflowService {
UPDATE inspection_reports
SET executive_summary=$2,
report_description=$3,
word_status=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN 'PENDING' ELSE word_status END,
word_original_name=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_original_name END,
word_stored_name=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_stored_name END,
word_mime_type=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_mime_type END,
word_size_bytes=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_size_bytes END,
word_sha256=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_sha256 END,
word_generated_at=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_generated_at END,
word_error=CASE
WHEN executive_summary IS DISTINCT FROM $2 OR report_description IS DISTINCT FROM $3
THEN NULL ELSE word_error END,
updated_at=CURRENT_TIMESTAMP
WHERE id=$1
`, [reportId, executiveSummary, reportDescription]);
@@ -195,6 +220,44 @@ export class InspectionReportWorkflowService {
}
}
async officialPdfContent(reportId: string): Promise<{ filePath: string; originalName: string; mimeType: string }> {
const [row] = await this.dataSource.query(`
SELECT
gedo_pdf_original_name AS "originalName",
gedo_pdf_stored_name AS "storedName",
gedo_pdf_mime_type AS "mimeType",
gedo_pdf_size_bytes::integer AS "sizeBytes",
gedo_pdf_sha256 AS sha256
FROM inspection_reports
WHERE id=$1
`, [reportId]) as Array<{
originalName: string | null;
storedName: string | null;
mimeType: string | null;
sizeBytes: number | null;
sha256: string | null;
}>;
if (!row) throw reportNotFound();
if (!row.originalName || !row.storedName || !row.sizeBytes || !row.sha256) {
throw new NotFoundException({
code: 'INSPECTION_REPORT_GEDO_PDF_NOT_AVAILABLE',
message: 'El PDF oficial de GEDO todavía no está disponible',
});
}
const filePath = resolve(this.root, row.storedName);
if (filePath === this.root || !filePath.startsWith(`${this.root}/`)) throw this.reportStorageError();
const fileStat = await stat(filePath).catch(() => null);
if (!fileStat?.isFile() || fileStat.size !== row.sizeBytes) throw this.reportStorageError();
const buffer = await readFile(filePath);
const sha256 = createHash('sha256').update(buffer).digest('hex');
if (sha256 !== row.sha256) throw this.reportStorageError();
return {
filePath,
originalName: row.originalName,
mimeType: row.mimeType ?? 'application/pdf',
};
}
async addFollowUp(
reportId: string,
dto: CreateInspectionReportFollowUpDto,
@@ -291,6 +354,13 @@ export class InspectionReportWorkflowService {
`, [reportId]);
}
private reportStorageError(): InternalServerErrorException {
return new InternalServerErrorException({
code: 'INSPECTION_REPORT_GEDO_PDF_STORAGE_ERROR',
message: 'El PDF oficial de GEDO no está disponible o no supera la validación de integridad',
});
}
private assertPdf(file: UploadedInspectionReportFile | undefined): void {
if (!file?.buffer?.length) {
throw new BadRequestException({