import { createHash } from 'node:crypto'; 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 { const out = String(value ?? '').trim(); return out || fallback; } function date(value: unknown): string { const parsed = new Date(String(value ?? '')); return Number.isFinite(parsed.getTime()) ? parsed.toLocaleDateString('es-AR') : '-'; } function clean(value: string): string { return value .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/[–—]/g, '-') .replace(/[“”]/g, '"') .replace(/[‘’]/g, "'") .replace(/[^\x20-\xFF]/g, '?'); } function escapePdf(value: string): string { return clean(value).replaceAll('\\', '\\\\').replaceAll('(', '\\(').replaceAll(')', '\\)'); } function wrap(value: string, max = 92): string[] { const words = clean(value).split(/\s+/).filter(Boolean); const out: string[] = []; let line = ''; for (const word of words) { const next = line ? `${line} ${word}` : word; if (next.length > max && line) { out.push(line); line = word; } else { line = next; } } if (line) out.push(line); return out.length ? out : ['-']; } function lockedSnapshot(snapshot: Record): { locked: Record; signatures: Array>; finalSha256: unknown; } { const sealed = asRecord(snapshot); const locked = asRecord(sealed.lockedSnapshot ?? sealed.preparedSnapshot); return { locked, signatures: asArray(sealed.signatures), finalSha256: sealed.finalSha256 ?? sealed.lockedSha256 ?? sealed.preparedSha256, }; } function urgencyLabel(value: unknown): string { return text(value, '') === 'URGENT' ? 'Urgente' : 'No urgente'; } function dayTypeLabel(value: unknown): string { return text(value, '') === 'CALENDAR' ? 'dias corridos' : 'dias habiles'; } function lines(snapshot: Record): string[] { const source = lockedSnapshot(snapshot); const locked = source.locked; const act = asRecord(locked.act); const inspection = asRecord(act.inspection ?? asRecord(act).visit); const responsible = asRecord(locked.responsible); const inventories = asArray(locked.inventories ?? locked.assets); const findings = asArray(locked.findings); const signatures = source.signatures; const companySignature = signatures.find((item) => text(item.signerType, '') === 'COMPANY_RESPONSIBLE'); const inspectorSignatures = signatures.filter((item) => text(item.signerType, '') === 'INSPECTOR'); const deadlineText = act.deadlineAt ? `${date(act.deadlineAt)} (${text(act.deadlineDays)} ${dayTypeLabel(act.deadlineDayType)})` : act.deadlineBasis === 'GEDO_DATE' ? `${text(act.deadlineDays)} ${dayTypeLabel(act.deadlineDayType)} desde fecha GEDO` : '-'; const out: string[] = [ 'ACTA DE INSPECCION', '', `Acta: ${text(act.code)}`, `Inspeccion: ${text(inspection.code)}`, `Fecha: ${date(act.occurredAt)}`, `Urgencia: ${urgencyLabel(act.urgency)}`, `Plazo: ${deadlineText}`, `Representante de la empresa: ${text(responsible.fullName)}`, `DNI: ${text(responsible.documentNumber)}`, `Cargo / funcion: ${text(responsible.position)}`, `Email: ${text(responsible.email)}`, '', 'RESUMEN', ...wrap(text(act.summary)), '', 'OBSERVACIONES', ...wrap(text(act.observations)), '', 'INVENTARIO INSPECCIONADO', ]; if (!inventories.length) out.push('-'); for (const item of inventories) { out.push(...wrap(`${text(item.code)} | ${text(item.name)} | ${text(item.typeName ?? item.typeCode)}`)); } out.push('', 'HALLAZGOS'); if (!findings.length) out.push('Sin hallazgos registrados.'); for (const item of findings) { const recurrence = item.isRecurrence ? ` | REINCIDENCIA${item.recurrenceOfFindingId ? ` de ${text(item.recurrenceOfFindingCode ?? item.recurrenceOfFindingId)}` : ''}` : ''; out.push(...wrap(`${text(item.code)} | ${text(item.title)}${recurrence}`)); out.push(...wrap(`Descripcion: ${text(item.description)}`)); if (item.legalBasis) out.push(...wrap(`Base legal: ${text(item.legalBasis)}`)); } out.push('', 'FIRMAS Y CONSTANCIAS'); if (!inspectorSignatures.length) out.push('Firma de inspector: pendiente.'); for (const signature of inspectorSignatures) { out.push(...wrap(`Inspector: ${text(signature.signerName)} | ${text(signature.status)} | ${date(signature.signedAt ?? signature.createdAt)}`)); } if (!companySignature) { out.push('Manifestacion de empresa: pendiente.'); } else if (text(companySignature.status, '') === 'SIGNED') { const manifestation = text(companySignature.companyManifestation, 'CONFORMITY'); out.push(manifestation === 'DISSENT' ? 'Empresa: firma en disconformidad' : 'Empresa: firma en conformidad'); if (manifestation === 'DISSENT') out.push(...wrap(text(companySignature.companyStatement))); } else { out.push(...wrap(`Empresa: ${text(companySignature.status)} - ${text(companySignature.reason)}`)); } out.push( '', 'INTEGRIDAD', `Hash del Acta sellada: ${text(source.finalSha256)}`, `Hash del contenido bloqueado: ${text(snapshot.lockedSha256 ?? snapshot.preparedSha256)}`, ); return out; } function objectBuffer(id: number, body: Buffer | string): Buffer { const data = Buffer.isBuffer(body) ? body : Buffer.from(body, 'latin1'); return Buffer.concat([ Buffer.from(`${id} 0 obj\n`, 'ascii'), data, Buffer.from('\nendobj\n', 'ascii'), ]); } export function buildInspectionActPdf(snapshot: Record): { buffer: Buffer; sha256: string } { const all = lines(snapshot); const chunks: Array = []; for (let index = 0; index < all.length; index += 56) chunks.push(all.slice(index, index + 56)); if (!chunks.length) chunks.push(['ACTA DE INSPECCION']); const pageCount = chunks.length; const pageIds = Array.from({ length: pageCount }, (_, index) => 4 + index * 2); const contentIds = Array.from({ length: pageCount }, (_, index) => 5 + index * 2); const objects: Buffer[] = []; objects.push(objectBuffer(1, '<< /Type /Catalog /Pages 2 0 R >>')); objects.push(objectBuffer(2, `<< /Type /Pages /Count ${pageCount} /Kids [${pageIds.map((id) => `${id} 0 R`).join(' ')}] >>`)); objects.push(objectBuffer(3, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>')); chunks.forEach((chunk, index) => { const content = chunk .map((line, lineIndex) => `${lineIndex === 0 ? '' : 'T* '}(${escapePdf(line)}) Tj`) .join('\n'); const stream = Buffer.from(`BT\n/F1 9 Tf\n40 800 Td\n12 TL\n${content}\nET`, 'latin1'); objects.push(objectBuffer( pageIds[index]!, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents ${contentIds[index]} 0 R >>`, )); objects.push(objectBuffer( contentIds[index]!, Buffer.concat([ Buffer.from(`<< /Length ${stream.length} >>\nstream\n`, 'ascii'), stream, Buffer.from('\nendstream', 'ascii'), ]), )); }); const header = Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary'); const offsets: number[] = [0]; let position = header.length; for (const object of objects) { offsets.push(position); position += object.length; } const xrefOffset = position; const xref = [ `xref\n0 ${objects.length + 1}\n`, '0000000000 65535 f \n', ...objects.map((_, index) => `${String(offsets[index + 1]).padStart(10, '0')} 00000 n \n`), ].join(''); const trailer = `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`; const buffer = Buffer.concat([header, ...objects, Buffer.from(xref + trailer, 'ascii')]); return { buffer, sha256: createHash('sha256').update(buffer).digest('hex') }; }