From eeb35da02c213cf33ec9b30e5e7092aee19dcd1d Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 21:32:50 -0300 Subject: [PATCH] F4: generate Act PDF only from SEALED Acts --- .../inspection-act-pdf.service.ts | 118 ++++++++++++++++-- 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/api-v3/src/inspection-reports/inspection-act-pdf.service.ts b/api-v3/src/inspection-reports/inspection-act-pdf.service.ts index 056731a..22cef80 100644 --- a/api-v3/src/inspection-reports/inspection-act-pdf.service.ts +++ b/api-v3/src/inspection-reports/inspection-act-pdf.service.ts @@ -8,20 +8,112 @@ import { buildInspectionActPdf } from './inspection-act-pdf-builder'; @Injectable() export class InspectionActPdfService { - private readonly root:string; - constructor(private readonly dataSource:DataSource, config:ConfigService){ const configured=config.get('INSPECTION_ACT_PDF_ROOT')??'/app/storage/asset-media/inspection-acts-pdf'; if(!isAbsolute(configured))throw new Error('INSPECTION_ACT_PDF_ROOT must be an absolute path'); this.root=resolve(configured); if(this.root===parse(this.root).root)throw new Error('INSPECTION_ACT_PDF_ROOT cannot be the filesystem root'); } + private readonly root: string; - async ensure(actId:string):Promise{ - const [row]=await this.dataSource.query(`SELECT a.id,a.code,a.closure_sha256 AS "closureSha256",c.final_snapshot AS "finalSnapshot",p.status,p.stored_name AS "storedName" FROM inspection_acts a JOIN inspection_act_closures c ON c.act_id=a.id LEFT JOIN inspection_act_pdf_artifacts p ON p.act_id=a.id WHERE a.id=$1 AND a.status='CLOSED'`,[actId]) as Array<{id:string;code:string;closureSha256:string;finalSnapshot:Record;status:string|null;storedName:string|null}>; - if(!row)throw new NotFoundException({code:'INSPECTION_ACT_NOT_CLOSED',message:'El acta cerrada no está disponible'}); - if(row.status==='READY'&&row.storedName){ try{ await this.content(actId); return; }catch{} } - await this.dataSource.query(`INSERT INTO inspection_act_pdf_artifacts (act_id,status) VALUES ($1,'PENDING') ON CONFLICT (act_id) DO UPDATE SET status='PENDING',error=NULL,updated_at=CURRENT_TIMESTAMP`,[actId]); - try{ - const snapshot={...row.finalSnapshot,finalSha256:row.closureSha256}; const built=buildInspectionActPdf(snapshot); await mkdir(this.root,{recursive:true,mode:0o700}); const storedName=`${actId}.pdf`; const originalName=`${row.code}.pdf`; const filePath=resolve(this.root,storedName); await writeFile(filePath,built.buffer,{mode:0o600}); - await this.dataSource.query(`UPDATE inspection_act_pdf_artifacts SET status='READY',original_name=$2,stored_name=$3,mime_type='application/pdf',size_bytes=$4,sha256=$5,generated_at=CURRENT_TIMESTAMP,error=NULL,updated_at=CURRENT_TIMESTAMP WHERE act_id=$1`,[actId,originalName,storedName,built.buffer.length,built.sha256]); - }catch(error){ const message=error instanceof Error?error.message.slice(0,500):'Error desconocido'; await this.dataSource.query(`UPDATE inspection_act_pdf_artifacts SET status='FAILED',error=$2,updated_at=CURRENT_TIMESTAMP WHERE act_id=$1`,[actId,message]).catch(()=>undefined); } + constructor(private readonly dataSource: DataSource, config: ConfigService) { + const configured = config.get('INSPECTION_ACT_PDF_ROOT') + ?? '/app/storage/asset-media/inspection-acts-pdf'; + if (!isAbsolute(configured)) throw new Error('INSPECTION_ACT_PDF_ROOT must be an absolute path'); + this.root = resolve(configured); + if (this.root === parse(this.root).root) throw new Error('INSPECTION_ACT_PDF_ROOT cannot be the filesystem root'); } - async content(actId:string):Promise<{buffer:Buffer;originalName:string;mimeType:string}>{ const [row]=await this.dataSource.query(`SELECT original_name AS "originalName",stored_name AS "storedName",mime_type AS "mimeType",size_bytes::integer AS "sizeBytes",sha256 FROM inspection_act_pdf_artifacts WHERE act_id=$1 AND status='READY'`,[actId]) as Array<{originalName:string;storedName:string;mimeType:string;sizeBytes:number;sha256:string}>; if(!row)throw new NotFoundException({code:'INSPECTION_ACT_PDF_NOT_READY',message:'El PDF del acta todavía no está disponible'}); const filePath=resolve(this.root,row.storedName); if(!filePath.startsWith(`${this.root}/`))throw this.storageError(); const st=await stat(filePath).catch(()=>null); if(!st?.isFile()||st.size!==row.sizeBytes)throw this.storageError(); const buffer=await readFile(filePath); if(createHash('sha256').update(buffer).digest('hex')!==row.sha256)throw this.storageError(); return {buffer,originalName:row.originalName,mimeType:row.mimeType}; } - private storageError(){return new InternalServerErrorException({code:'INSPECTION_ACT_PDF_STORAGE_ERROR',message:'El PDF del acta no está disponible o no supera la validación de integridad'});} + async ensure(actId: string): Promise { + const [row] = await this.dataSource.query(` + SELECT + act.id, + act.code, + act.closure_sha256 AS "closureSha256", + closure.final_snapshot AS "finalSnapshot", + artifact.status, + artifact.stored_name AS "storedName" + FROM inspection_acts act + JOIN inspection_act_closures closure ON closure.act_id=act.id + LEFT JOIN inspection_act_pdf_artifacts artifact ON artifact.act_id=act.id + WHERE act.id=$1 AND act.status='SEALED' + `, [actId]) as Array<{ + id: string; + code: string; + closureSha256: string; + finalSnapshot: Record; + status: string | null; + storedName: string | null; + }>; + if (!row) { + throw new NotFoundException({ + code: 'INSPECTION_ACT_NOT_SEALED', + message: 'El PDF sólo puede generarse desde un Acta SELLADA', + }); + } + if (row.status === 'READY' && row.storedName) { + try { + await this.content(actId); + return; + } catch {} + } + await this.dataSource.query(` + INSERT INTO inspection_act_pdf_artifacts(act_id,status) + VALUES($1,'PENDING') + ON CONFLICT (act_id) DO UPDATE SET + status='PENDING',error=NULL,updated_at=CURRENT_TIMESTAMP + `, [actId]); + try { + const snapshot = { ...row.finalSnapshot, finalSha256: row.closureSha256 }; + const built = buildInspectionActPdf(snapshot); + await mkdir(this.root, { recursive: true, mode: 0o700 }); + const storedName = `${actId}.pdf`; + const originalName = `${row.code}.pdf`; + const filePath = resolve(this.root, storedName); + await writeFile(filePath, built.buffer, { mode: 0o600 }); + await this.dataSource.query(` + UPDATE inspection_act_pdf_artifacts + SET status='READY',original_name=$2,stored_name=$3,mime_type='application/pdf', + size_bytes=$4,sha256=$5,generated_at=CURRENT_TIMESTAMP,error=NULL, + updated_at=CURRENT_TIMESTAMP + WHERE act_id=$1 + `, [actId, originalName, storedName, built.buffer.length, built.sha256]); + } catch (error) { + const message = error instanceof Error ? error.message.slice(0, 500) : 'Error desconocido'; + await this.dataSource.query(` + UPDATE inspection_act_pdf_artifacts + SET status='FAILED',error=$2,updated_at=CURRENT_TIMESTAMP + WHERE act_id=$1 + `, [actId, message]).catch(() => undefined); + } + } + + async content(actId: string): Promise<{ buffer: Buffer; originalName: string; mimeType: string }> { + const [row] = await this.dataSource.query(` + SELECT original_name AS "originalName",stored_name AS "storedName", + mime_type AS "mimeType",size_bytes::integer AS "sizeBytes",sha256 + FROM inspection_act_pdf_artifacts + WHERE act_id=$1 AND status='READY' + `, [actId]) as Array<{ + originalName: string; + storedName: string; + mimeType: string; + sizeBytes: number; + sha256: string; + }>; + if (!row) { + throw new NotFoundException({ + code: 'INSPECTION_ACT_PDF_NOT_READY', + message: 'El PDF del Acta todavía no está disponible', + }); + } + const filePath = resolve(this.root, row.storedName); + if (!filePath.startsWith(`${this.root}/`)) throw this.storageError(); + const fileStat = await stat(filePath).catch(() => null); + if (!fileStat?.isFile() || fileStat.size !== row.sizeBytes) throw this.storageError(); + const buffer = await readFile(filePath); + if (createHash('sha256').update(buffer).digest('hex') !== row.sha256) throw this.storageError(); + return { buffer, originalName: row.originalName, mimeType: row.mimeType }; + } + + private storageError(): InternalServerErrorException { + return new InternalServerErrorException({ + code: 'INSPECTION_ACT_PDF_STORAGE_ERROR', + message: 'El PDF del Acta no está disponible o no supera la validación de integridad', + }); + } }