import { createHash } from 'node:crypto'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import PDFDocument = require('pdfkit'); export interface ActPdfImage { id: string; findingId?: string; assetId?: string; signerName?: string; title?: string; capturedAt?: string | Date | null; sha256: string; buffer: Buffer; } function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function asArray(value: unknown): Array> { return Array.isArray(value) ? value.map(asRecord) : []; } function text(value: unknown, fallback = ''): string { return String(value ?? '').trim() || fallback; } function date(value: unknown): string { const parsed = new Date(String(value ?? '')); return Number.isFinite(parsed.getTime()) ? parsed.toLocaleString('es-AR', { timeZone: 'America/Argentina/Mendoza', dateStyle: 'short', timeStyle: 'short' }) : '-'; } function isPlaceholder(value: unknown): boolean { return text(value).startsWith('Acta de inspección en curso. Los Hallazgos'); } // %PDF-1.4 is the document-version contract for consolidated Actas. export async function buildInspectionActPdf(snapshot: Record, images: ActPdfImage[] = [], context: { companyName?: string | null; areaName?: string | null; scopeName?: string | null } = {}): Promise<{ buffer: Buffer; sha256: string }> { const sealed = asRecord(snapshot); const locked = asRecord(sealed.lockedSnapshot ?? sealed.preparedSnapshot); const act = asRecord(locked.act); const inspection = asRecord(act.inspection ?? act.visit); const responsible = asRecord(locked.responsible); const findings = asArray(locked.findings); const signatures = asArray(sealed.signatures); const hash = text(sealed.finalSha256 ?? sealed.lockedSha256); const logo = resolve(process.cwd(), 'assets/logo-mendoza.png'); const doc = new PDFDocument({ size: 'A4', pdfVersion: '1.4', margins: { top: 146, bottom: 80, left: 54, right: 54 }, compress: true }); doc.registerFont('body', resolve(process.cwd(), 'assets/fonts/DejaVuSans.ttf')); doc.registerFont('body-bold', resolve(process.cwd(), 'assets/fonts/DejaVuSans-Bold.ttf')); const chunks: Buffer[] = []; doc.on('data', (part: Buffer) => chunks.push(part)); const done = new Promise((complete, reject) => { doc.on('end', () => complete(Buffer.concat(chunks))); doc.on('error', reject); }); const blue = '#162D69'; const header = () => { doc.font('body-bold').fillColor(blue).fontSize(11).text('MINISTERIO DE ENERGÍA Y AMBIENTE', 54, 42); doc.text('DIRECCIÓN DE HIDROCARBUROS', 54, 58); if (existsSync(logo)) doc.image(logo, 474, 32, { fit: [62, 82] }); doc.moveTo(54, 121).lineTo(540, 121).strokeColor('#BAC5DA').stroke(); doc.y = 146; }; doc.on('pageAdded', header); header(); const need = (height: number) => { if (doc.y + height > doc.page.height - 85) doc.addPage(); }; const heading = (label: string) => { need(48); doc.moveDown(1); doc.font('body-bold').fillColor(blue).fontSize(12).text(label.toUpperCase()); doc.moveDown(0.35); }; const body = (value: unknown) => { need(22); doc.font('body').fillColor('#202939').fontSize(10.5).text(text(value, '-'), { lineGap: 3 }); doc.moveDown(0.4); }; const label = (name: string, value: unknown) => { if (!text(value)) return; need(22); doc.font('body-bold').fillColor('#202939').fontSize(10).text(`${name}: `, { continued: true }); doc.font('body').text(text(value)); doc.moveDown(0.35); }; const image = (entry: ActPdfImage, caption: string) => { need(235); const y = doc.y; doc.image(entry.buffer, 58, y, { fit: [470, 190] }); doc.y = y + 195; doc.font('body').fontSize(8).fillColor('#47536A').text(`${caption} · SHA-256 ${entry.sha256}`, 58, doc.y, { width: 475 }); doc.moveDown(0.5); }; doc.font('body-bold').fillColor('#202939').fontSize(19).text(`ACTA DE INSPECCIÓN ${text(act.code)}`); doc.moveDown(0.5); label('Inspección', inspection.code); label('Empresa inspeccionada', context.companyName); label('Área', context.areaName); label('Yacimiento o instalación', context.scopeName); label('Fecha y hora', date(act.occurredAt)); label('Urgencia del Acta', text(act.urgency) === 'URGENT' ? 'Urgente' : text(act.urgency) === 'NON_URGENT' ? 'No urgente' : ''); label('Representante de la empresa', responsible.fullName); label('DNI', responsible.documentNumber); label('Cargo o función', responsible.position); heading('Lo actuado'); if (text(act.summary) && !isPlaceholder(act.summary)) body(act.summary); else body(`Se realizó la inspección ${text(inspection.code)}. El contenido constatado se detalla en los hallazgos registrados a continuación.`); if (act.observations) { label('Observaciones', act.observations); } heading('Hallazgos y fotografías'); if (!findings.length) body('No se registraron hallazgos.'); for (const finding of findings) { need(75); doc.font('body-bold').fillColor(blue).fontSize(11).text(`${text(finding.code)} · ${text(finding.title)}`); label('Descripción', finding.description); if (finding.legalBasis) label('Normativa consignada', finding.legalBasis); if (finding.severity != null) label('Gravedad', `${text(finding.severity)}/10`); for (const photo of images.filter((item) => item.findingId === text(finding.id))) { image(photo, `Fotografía del hallazgo ${text(finding.code)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`); } doc.moveDown(0.4); } heading('Intervinientes y firmas'); for (const signature of signatures) { const name = text(signature.signerName); const role = text(signature.signerType) === 'INSPECTOR' ? 'Inspector/a' : 'Representante de la empresa'; const status = text(signature.status); label(role, `${name} · ${status === 'SIGNED' ? 'Firmó' : status === 'REFUSED' ? 'Se negó a firmar' : 'No firmó'} · ${date(signature.signedAt ?? signature.createdAt)}`); if (signature.companyManifestation === 'DISSENT') label('Disconformidad', signature.companyStatement); if (status === 'REFUSED') label('Motivo de negativa', signature.reason); const signatureImage = images.find((item) => item.signerName === name && item.sha256 === text(signature.imageSha256)); if (signatureImage) { need(100); const y = doc.y; doc.image(signatureImage.buffer, 60, y, { fit: [230, 60] }); doc.y = y + 66; } } heading('Integridad del Acta'); body(`SHA-256 del cierre: ${hash}`); body(`SHA-256 del contenido cerrado: ${text(sealed.lockedSha256)}`); need(25); doc.fontSize(8).fillColor('#637088').text(`Acta ${text(act.code)} · documento consolidado · ${date(asRecord(sealed.seal).serverSealedAt)}`, 54, doc.y); doc.end(); const buffer = await done; return { buffer, sha256: createHash('sha256').update(buffer).digest('hex') }; }