From 463734f5b0c3fa8ec84d6b747ba3df8fcc2db461 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 15:24:27 -0300 Subject: [PATCH] F3.1: enviar copia del Acta al Inspector responsable --- .../inspection-document-delivery.service.ts | 484 +++++++++++++++++- 1 file changed, 457 insertions(+), 27 deletions(-) diff --git a/api-v3/src/inspection-reports/inspection-document-delivery.service.ts b/api-v3/src/inspection-reports/inspection-document-delivery.service.ts index 8a63220..d855648 100644 --- a/api-v3/src/inspection-reports/inspection-document-delivery.service.ts +++ b/api-v3/src/inspection-reports/inspection-document-delivery.service.ts @@ -1,40 +1,470 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { DataSource } from 'typeorm'; import { ConfigService } from '@nestjs/config'; +import { DataSource } from 'typeorm'; +import { administrationAuditContext } from '../administration/common/administration-audit'; +import { AuditService } from '../audit/audit.service'; +import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context'; +import { AuditAction, AuditSource } from '../database/entities'; +import type { UpdateDocumentDeliverySettingsDto } from './dto/update-document-delivery-settings.dto'; import { InspectionActPdfService } from './inspection-act-pdf.service'; import { InspectionReportWordService } from './inspection-report-word.service'; import { SmtpDeliveryService } from './smtp-delivery.service'; -import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context'; -import { AuditService } from '../audit/audit.service'; -import { administrationAuditContext } from '../administration/common/administration-audit'; -import { AuditAction, AuditSource } from '../database/entities'; -import type { UpdateDocumentDeliverySettingsDto } from './dto/update-document-delivery-settings.dto'; -export interface DeliveryRow { id:string; actId:string; reportId:string|null; documentKind:'ACT_PDF'|'REPORT_WORD'; recipientKind:'COMPANY'|'OFFICE'|'DIRECTOR'; recipientAssetId:string|null; recipientEmail:string|null; status:string; attempts:number; actCode:string; reportCode:string|null; } +export type DeliveryRecipientKind = 'COMPANY' | 'OFFICE' | 'DIRECTOR' | 'INSPECTOR'; + +export interface DeliveryRow { + id: string; + actId: string; + reportId: string | null; + documentKind: 'ACT_PDF' | 'REPORT_WORD'; + recipientKind: DeliveryRecipientKind; + recipientAssetId: string | null; + recipientUserId: string | null; + recipientEmail: string | null; + status: string; + attempts: number; + actCode: string; + reportCode: string | null; +} @Injectable() export class InspectionDocumentDeliveryService { - constructor(private readonly dataSource:DataSource, private readonly pdf:InspectionActPdfService, private readonly word:InspectionReportWordService, private readonly smtp:SmtpDeliveryService, private readonly audit:AuditService, private readonly config:ConfigService){} + constructor( + private readonly dataSource: DataSource, + private readonly pdf: InspectionActPdfService, + private readonly word: InspectionReportWordService, + private readonly smtp: SmtpDeliveryService, + private readonly audit: AuditService, + private readonly config: ConfigService, + ) {} - async settings(){ const [row]=await this.dataSource.query(`SELECT office_email AS "officeEmail",director_email AS "directorEmail",updated_at AS "updatedAt" FROM institutional_delivery_settings WHERE id=1`) as Array<{officeEmail:string|null;directorEmail:string|null;updatedAt:Date}>; return {...row,smtpConfigured:this.smtp.configured(),mailFrom:this.config.get('MAIL_FROM')??null}; } - async updateSettings(dto:UpdateDocumentDeliverySettingsDto,principal:AuthPrincipal,request:RequestWithContext){ const before=await this.settings(); await this.dataSource.query(`UPDATE institutional_delivery_settings SET office_email=$1,director_email=$2,updated_by=$3,updated_at=CURRENT_TIMESTAMP WHERE id=1`,[dto.officeEmail??null,dto.directorEmail??null,principal.userId]); const after=await this.settings(); await this.audit.record({...administrationAuditContext(principal,request),action:AuditAction.DOCUMENT_DELIVERY_SETTINGS_UPDATED,entityType:'institutional_delivery_settings',entityId:'1',beforeData:before,afterData:after}); return after; } - async list(){ const data=await this.dataSource.query(`SELECT d.id,d.act_id AS "actId",d.report_id AS "reportId",d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",d.recipient_asset_id AS "recipientAssetId",d.recipient_email AS "recipientEmail",d.status,d.attempts,d.last_attempt_at AS "lastAttemptAt",d.sent_at AS "sentAt",d.provider_message_id AS "providerMessageId",d.last_error AS "lastError",d.created_at AS "createdAt",a.code AS "actCode",r.code AS "reportCode",recipient.name AS "recipientAssetName" FROM inspection_document_deliveries d JOIN inspection_acts a ON a.id=d.act_id LEFT JOIN inspection_reports r ON r.id=d.report_id LEFT JOIN assets recipient ON recipient.id=d.recipient_asset_id ORDER BY d.created_at DESC LIMIT 200`); return {data}; } - - async dispatchForAct(actId:string):Promise{ await this.pdf.ensure(actId).catch(()=>undefined); const [report]=await this.dataSource.query(`SELECT id FROM inspection_reports WHERE act_id=$1`,[actId]) as Array<{id:string}>; if(report)await this.word.ensure(report.id); await this.ensureRows(actId,report?.id??null); const rows=await this.rowsForAct(actId); for(const row of rows)await this.attempt(row).catch(()=>undefined); } - async retry(id:string,principal:AuthPrincipal,request:RequestWithContext):Promise{ const row=await this.load(id); await this.audit.record({...administrationAuditContext(principal,request),action:AuditAction.DOCUMENT_DELIVERY_RETRY_REQUESTED,entityType:'inspection_document_delivery',entityId:id,metadata:{actId:row.actId,documentKind:row.documentKind,recipientKind:row.recipientKind}}); await this.refreshRecipient(row); await this.attempt(await this.load(id)); return this.load(id); } - async retryPending(principal:AuthPrincipal,request:RequestWithContext){ const rows=await this.dataSource.query(`SELECT id FROM inspection_document_deliveries WHERE status<>'SENT' ORDER BY created_at ASC LIMIT 100`) as Array<{id:string}>; for(const item of rows)await this.retry(item.id,principal,request).catch(()=>undefined); return {processed:rows.length}; } - - private async ensureRows(actId:string,reportId:string|null){ const [settings]=await this.dataSource.query(`SELECT office_email AS "officeEmail",director_email AS "directorEmail" FROM institutional_delivery_settings WHERE id=1`) as Array<{officeEmail:string|null;directorEmail:string|null}>; const companies=await this.dataSource.query(`SELECT DISTINCT company.id,profile.notification_email AS email FROM inspection_act_assets link JOIN assets asset ON asset.id=link.asset_id JOIN asset_types asset_type ON asset_type.id=asset.asset_type_id JOIN assets company ON company.id=COALESCE(asset.operator_company_id,CASE WHEN asset_type.operational_role='COMPANY' THEN asset.id END) LEFT JOIN organization_profiles profile ON profile.asset_id=company.id WHERE link.act_id=$1 AND link.included=true`,[actId]) as Array<{id:string;email:string|null}>; - for(const company of companies)await this.upsertRow(actId,reportId,'ACT_PDF','COMPANY',company.id,company.email); - await this.upsertRow(actId,reportId,'ACT_PDF','OFFICE',null,settings?.officeEmail??null); - if(reportId)await this.upsertRow(actId,reportId,'REPORT_WORD','DIRECTOR',null,settings?.directorEmail??null); + async settings() { + const [row] = await this.dataSource.query(` + SELECT + office_email AS "officeEmail", + director_email AS "directorEmail", + updated_at AS "updatedAt" + FROM institutional_delivery_settings + WHERE id=1 + `) as Array<{ + officeEmail: string | null; + directorEmail: string | null; + updatedAt: Date; + }>; + return { + ...row, + smtpConfigured: this.smtp.configured(), + mailFrom: this.config.get('MAIL_FROM') ?? null, + }; } - private async upsertRow(actId:string,reportId:string|null,documentKind:string,recipientKind:string,recipientAssetId:string|null,recipientEmail:string|null){ await this.dataSource.query(`INSERT INTO inspection_document_deliveries (act_id,report_id,document_kind,recipient_kind,recipient_asset_id,recipient_key,recipient_email,status) VALUES ($1,$2,$3,$4,$5,COALESCE($5::uuid,'00000000-0000-0000-0000-000000000000'::uuid),$6,$7) ON CONFLICT (act_id,document_kind,recipient_kind,recipient_key) DO UPDATE SET report_id=COALESCE(EXCLUDED.report_id,inspection_document_deliveries.report_id),recipient_email=COALESCE(inspection_document_deliveries.recipient_email,EXCLUDED.recipient_email),updated_at=CURRENT_TIMESTAMP`,[actId,reportId,documentKind,recipientKind,recipientAssetId,recipientEmail,recipientEmail?'PENDING':'WAITING_RECIPIENT']); } - private async rowsForAct(actId:string){ return this.dataSource.query(`SELECT d.id,d.act_id AS "actId",d.report_id AS "reportId",d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",d.recipient_asset_id AS "recipientAssetId",d.recipient_email AS "recipientEmail",d.status,d.attempts,a.code AS "actCode",r.code AS "reportCode" FROM inspection_document_deliveries d JOIN inspection_acts a ON a.id=d.act_id LEFT JOIN inspection_reports r ON r.id=d.report_id WHERE d.act_id=$1 ORDER BY d.created_at`,[actId]) as Promise; } - private async load(id:string){ const [row]=await this.dataSource.query(`SELECT d.id,d.act_id AS "actId",d.report_id AS "reportId",d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",d.recipient_asset_id AS "recipientAssetId",d.recipient_email AS "recipientEmail",d.status,d.attempts,a.code AS "actCode",r.code AS "reportCode" FROM inspection_document_deliveries d JOIN inspection_acts a ON a.id=d.act_id LEFT JOIN inspection_reports r ON r.id=d.report_id WHERE d.id=$1`,[id]) as DeliveryRow[]; if(!row)throw new NotFoundException({code:'DOCUMENT_DELIVERY_NOT_FOUND',message:'Entrega documental no encontrada'}); return row; } - private async refreshRecipient(row:DeliveryRow){ let email:string|null=null; if(row.recipientKind==='COMPANY'&&row.recipientAssetId){ const [company]=await this.dataSource.query(`SELECT notification_email AS email FROM organization_profiles WHERE asset_id=$1`,[row.recipientAssetId]) as Array<{email:string|null}>; email=company?.email??null; } else { const [settings]=await this.dataSource.query(`SELECT office_email AS "officeEmail",director_email AS "directorEmail" FROM institutional_delivery_settings WHERE id=1`) as Array<{officeEmail:string|null;directorEmail:string|null}>; email=row.recipientKind==='OFFICE'?settings?.officeEmail??null:settings?.directorEmail??null; } await this.dataSource.query(`UPDATE inspection_document_deliveries SET recipient_email=$2,status=CASE WHEN $2::text IS NULL THEN 'WAITING_RECIPIENT' ELSE 'PENDING' END,last_error=NULL,updated_at=CURRENT_TIMESTAMP WHERE id=$1 AND status<>'SENT'`,[row.id,email]); } - private async attempt(row:DeliveryRow){ if(row.status==='SENT')return; if(!row.recipientEmail){await this.setStatus(row.id,'WAITING_RECIPIENT','Destinatario no configurado');return;} if(!this.smtp.configured()){await this.setStatus(row.id,'WAITING_TRANSPORT','SMTP no configurado');return;} let attachment:{filename:string;mimeType:string;content:Buffer}; try{ if(row.documentKind==='ACT_PDF'){await this.pdf.ensure(row.actId); const file=await this.pdf.content(row.actId); attachment={filename:file.originalName,mimeType:file.mimeType,content:file.buffer};}else{if(!row.reportId)throw new Error('Informe no vinculado'); await this.word.ensure(row.reportId); const file=await this.word.content(row.reportId); const {readFile}=await import('node:fs/promises'); attachment={filename:file.originalName,mimeType:file.mimeType,content:await readFile(file.filePath)};}}catch(error){await this.setStatus(row.id,'WAITING_ARTIFACT',error instanceof Error?error.message:'Documento no disponible');return;} - await this.dataSource.query(`UPDATE inspection_document_deliveries SET attempts=attempts+1,last_attempt_at=CURRENT_TIMESTAMP,status='PENDING',last_error=NULL,updated_at=CURRENT_TIMESTAMP WHERE id=$1`,[row.id]); try{ const label=row.documentKind==='ACT_PDF'?`Acta ${row.actCode}`:`Informe ${row.reportCode??''}`; const sent=await this.smtp.send({to:row.recipientEmail,subject:`DH Inspección · ${label}`,text:row.documentKind==='ACT_PDF'?`Se adjunta el acta cerrada e inmutable ${row.actCode}.`:`Se adjunta el informe Word automático ${row.reportCode??''} para revisión del Director de Hidrocarburos.`,attachment}); await this.dataSource.query(`UPDATE inspection_document_deliveries SET status='SENT',sent_at=CURRENT_TIMESTAMP,provider_message_id=$2,last_error=NULL,updated_at=CURRENT_TIMESTAMP WHERE id=$1`,[row.id,sent.messageId]); await this.audit.record({action:AuditAction.DOCUMENT_DELIVERY_SENT,entityType:'inspection_document_delivery',entityId:row.id,source:AuditSource.SYSTEM,actorUserId:null,actorUsername:null,requestId:null,ip:null,userAgent:null,beforeData:null,afterData:{recipientKind:row.recipientKind,documentKind:row.documentKind,status:'SENT'},metadata:{actId:row.actId,reportId:row.reportId}}); }catch(error){await this.setStatus(row.id,'FAILED',error instanceof Error?error.message:'Error de entrega');} + + async updateSettings( + dto: UpdateDocumentDeliverySettingsDto, + principal: AuthPrincipal, + request: RequestWithContext, + ) { + const before = await this.settings(); + await this.dataSource.query(` + UPDATE institutional_delivery_settings + SET office_email=$1,director_email=$2,updated_by=$3,updated_at=CURRENT_TIMESTAMP + WHERE id=1 + `, [dto.officeEmail ?? null, dto.directorEmail ?? null, principal.userId]); + const after = await this.settings(); + await this.audit.record({ + ...administrationAuditContext(principal, request), + action: AuditAction.DOCUMENT_DELIVERY_SETTINGS_UPDATED, + entityType: 'institutional_delivery_settings', + entityId: '1', + beforeData: before, + afterData: after, + }); + return after; + } + + async list() { + const data = await this.dataSource.query(` + SELECT + d.id, + d.act_id AS "actId", + d.report_id AS "reportId", + d.document_kind AS "documentKind", + d.recipient_kind AS "recipientKind", + d.recipient_asset_id AS "recipientAssetId", + d.recipient_user_id AS "recipientUserId", + d.recipient_email AS "recipientEmail", + d.status, + d.attempts, + d.last_attempt_at AS "lastAttemptAt", + d.sent_at AS "sentAt", + d.provider_message_id AS "providerMessageId", + d.last_error AS "lastError", + d.created_at AS "createdAt", + a.code AS "actCode", + r.code AS "reportCode", + recipient.name AS "recipientAssetName", + CASE + WHEN recipient_user.id IS NULL THEN NULL + ELSE btrim(concat_ws(' ', recipient_user.first_name, recipient_user.last_name)) + END AS "recipientUserName" + FROM inspection_document_deliveries d + JOIN inspection_acts a ON a.id=d.act_id + LEFT JOIN inspection_reports r ON r.id=d.report_id + LEFT JOIN assets recipient ON recipient.id=d.recipient_asset_id + LEFT JOIN users recipient_user ON recipient_user.id=d.recipient_user_id + ORDER BY d.created_at DESC + LIMIT 200 + `); + return { data }; + } + + async dispatchForAct(actId: string): Promise { + await this.pdf.ensure(actId).catch(() => undefined); + const [report] = await this.dataSource.query( + `SELECT id FROM inspection_reports WHERE act_id=$1`, + [actId], + ) as Array<{ id: string }>; + if (report) await this.word.ensure(report.id); + await this.ensureRows(actId, report?.id ?? null); + const rows = await this.rowsForAct(actId); + for (const row of rows) await this.attempt(row).catch(() => undefined); + } + + async retry( + id: string, + principal: AuthPrincipal, + request: RequestWithContext, + ): Promise { + const row = await this.load(id); + await this.audit.record({ + ...administrationAuditContext(principal, request), + action: AuditAction.DOCUMENT_DELIVERY_RETRY_REQUESTED, + entityType: 'inspection_document_delivery', + entityId: id, + metadata: { + actId: row.actId, + documentKind: row.documentKind, + recipientKind: row.recipientKind, + recipientUserId: row.recipientUserId, + }, + }); + await this.refreshRecipient(row); + await this.attempt(await this.load(id)); + return this.load(id); + } + + async retryPending(principal: AuthPrincipal, request: RequestWithContext) { + const rows = await this.dataSource.query(` + SELECT id + FROM inspection_document_deliveries + WHERE status<>'SENT' + ORDER BY created_at ASC + LIMIT 100 + `) as Array<{ id: string }>; + for (const item of rows) await this.retry(item.id, principal, request).catch(() => undefined); + return { processed: rows.length }; + } + + private async ensureRows(actId: string, reportId: string | null) { + const [settings] = await this.dataSource.query(` + SELECT office_email AS "officeEmail",director_email AS "directorEmail" + FROM institutional_delivery_settings + WHERE id=1 + `) as Array<{ officeEmail: string | null; directorEmail: string | null }>; + + const companies = await this.dataSource.query(` + SELECT DISTINCT company.id,profile.notification_email AS email + FROM inspection_act_assets link + JOIN assets asset ON asset.id=link.asset_id + JOIN asset_types asset_type ON asset_type.id=asset.asset_type_id + JOIN assets company ON company.id=COALESCE( + asset.operator_company_id, + CASE WHEN asset_type.operational_role='COMPANY' THEN asset.id END + ) + LEFT JOIN organization_profiles profile ON profile.asset_id=company.id + WHERE link.act_id=$1 AND link.included=true + `, [actId]) as Array<{ id: string; email: string | null }>; + + const [inspector] = await this.dataSource.query(` + SELECT + inspector.id, + inspector.email + FROM inspection_acts act + JOIN inspection_visits visit ON visit.id=act.visit_id + JOIN users inspector ON inspector.id=visit.lead_inspector_user_id + WHERE act.id=$1 + `, [actId]) as Array<{ id: string; email: string | null }>; + + for (const company of companies) { + await this.upsertRow({ + actId, + reportId, + documentKind: 'ACT_PDF', + recipientKind: 'COMPANY', + recipientAssetId: company.id, + recipientUserId: null, + recipientKey: company.id, + recipientEmail: company.email, + }); + } + + await this.upsertRow({ + actId, + reportId, + documentKind: 'ACT_PDF', + recipientKind: 'OFFICE', + recipientAssetId: null, + recipientUserId: null, + recipientKey: '00000000-0000-0000-0000-000000000000', + recipientEmail: settings?.officeEmail ?? null, + }); + + if (inspector) { + await this.upsertRow({ + actId, + reportId, + documentKind: 'ACT_PDF', + recipientKind: 'INSPECTOR', + recipientAssetId: null, + recipientUserId: inspector.id, + recipientKey: inspector.id, + recipientEmail: inspector.email, + }); + } + + if (reportId) { + await this.upsertRow({ + actId, + reportId, + documentKind: 'REPORT_WORD', + recipientKind: 'DIRECTOR', + recipientAssetId: null, + recipientUserId: null, + recipientKey: '00000000-0000-0000-0000-000000000000', + recipientEmail: settings?.directorEmail ?? null, + }); + } + } + + private async upsertRow(input: { + actId: string; + reportId: string | null; + documentKind: 'ACT_PDF' | 'REPORT_WORD'; + recipientKind: DeliveryRecipientKind; + recipientAssetId: string | null; + recipientUserId: string | null; + recipientKey: string; + recipientEmail: string | null; + }) { + const initialStatus = input.recipientEmail ? 'PENDING' : 'WAITING_RECIPIENT'; + await this.dataSource.query(` + INSERT INTO inspection_document_deliveries ( + act_id,report_id,document_kind,recipient_kind, + recipient_asset_id,recipient_user_id,recipient_key,recipient_email,status + ) VALUES ($1,$2,$3,$4,$5,$6,$7::uuid,$8,$9) + ON CONFLICT (act_id,document_kind,recipient_kind,recipient_key) DO UPDATE SET + report_id=COALESCE(EXCLUDED.report_id,inspection_document_deliveries.report_id), + recipient_asset_id=COALESCE(EXCLUDED.recipient_asset_id,inspection_document_deliveries.recipient_asset_id), + recipient_user_id=COALESCE(EXCLUDED.recipient_user_id,inspection_document_deliveries.recipient_user_id), + recipient_email=CASE + WHEN inspection_document_deliveries.status='SENT' THEN inspection_document_deliveries.recipient_email + ELSE EXCLUDED.recipient_email + END, + status=CASE + WHEN inspection_document_deliveries.status='SENT' THEN 'SENT' + WHEN EXCLUDED.recipient_email IS NULL THEN 'WAITING_RECIPIENT' + ELSE inspection_document_deliveries.status + END, + updated_at=CURRENT_TIMESTAMP + `, [ + input.actId, + input.reportId, + input.documentKind, + input.recipientKind, + input.recipientAssetId, + input.recipientUserId, + input.recipientKey, + input.recipientEmail, + initialStatus, + ]); + } + + private async rowsForAct(actId: string): Promise { + return this.dataSource.query(` + SELECT + d.id,d.act_id AS "actId",d.report_id AS "reportId", + d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind", + d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId", + d.recipient_email AS "recipientEmail",d.status,d.attempts, + a.code AS "actCode",r.code AS "reportCode" + FROM inspection_document_deliveries d + JOIN inspection_acts a ON a.id=d.act_id + LEFT JOIN inspection_reports r ON r.id=d.report_id + WHERE d.act_id=$1 + ORDER BY d.created_at + `, [actId]) as Promise; + } + + private async load(id: string): Promise { + const [row] = await this.dataSource.query(` + SELECT + d.id,d.act_id AS "actId",d.report_id AS "reportId", + d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind", + d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId", + d.recipient_email AS "recipientEmail",d.status,d.attempts, + a.code AS "actCode",r.code AS "reportCode" + FROM inspection_document_deliveries d + JOIN inspection_acts a ON a.id=d.act_id + LEFT JOIN inspection_reports r ON r.id=d.report_id + WHERE d.id=$1 + `, [id]) as DeliveryRow[]; + if (!row) { + throw new NotFoundException({ + code: 'DOCUMENT_DELIVERY_NOT_FOUND', + message: 'Entrega documental no encontrada', + }); + } + return row; + } + + private async refreshRecipient(row: DeliveryRow) { + let email: string | null = null; + if (row.recipientKind === 'COMPANY' && row.recipientAssetId) { + const [company] = await this.dataSource.query(` + SELECT notification_email AS email + FROM organization_profiles + WHERE asset_id=$1 + `, [row.recipientAssetId]) as Array<{ email: string | null }>; + email = company?.email ?? null; + } else if (row.recipientKind === 'INSPECTOR' && row.recipientUserId) { + const [inspector] = await this.dataSource.query(` + SELECT email + FROM users + WHERE id=$1 AND is_active=true + `, [row.recipientUserId]) as Array<{ email: string | null }>; + email = inspector?.email ?? null; + } else { + const [settings] = await this.dataSource.query(` + SELECT office_email AS "officeEmail",director_email AS "directorEmail" + FROM institutional_delivery_settings + WHERE id=1 + `) as Array<{ officeEmail: string | null; directorEmail: string | null }>; + email = row.recipientKind === 'OFFICE' + ? settings?.officeEmail ?? null + : settings?.directorEmail ?? null; + } + + await this.dataSource.query(` + UPDATE inspection_document_deliveries + SET recipient_email=$2, + status=CASE WHEN $2::text IS NULL THEN 'WAITING_RECIPIENT' ELSE 'PENDING' END, + last_error=NULL, + updated_at=CURRENT_TIMESTAMP + WHERE id=$1 AND status<>'SENT' + `, [row.id, email]); + } + + private async attempt(row: DeliveryRow) { + if (row.status === 'SENT') return; + if (!row.recipientEmail) { + await this.setStatus(row.id, 'WAITING_RECIPIENT', 'Destinatario no configurado'); + return; + } + if (!this.smtp.configured()) { + await this.setStatus(row.id, 'WAITING_TRANSPORT', 'SMTP no configurado'); + return; + } + + let attachment: { filename: string; mimeType: string; content: Buffer }; + try { + if (row.documentKind === 'ACT_PDF') { + await this.pdf.ensure(row.actId); + const file = await this.pdf.content(row.actId); + attachment = { + filename: file.originalName, + mimeType: file.mimeType, + content: file.buffer, + }; + } else { + if (!row.reportId) throw new Error('Informe no vinculado'); + await this.word.ensure(row.reportId); + const file = await this.word.content(row.reportId); + const { readFile } = await import('node:fs/promises'); + attachment = { + filename: file.originalName, + mimeType: file.mimeType, + content: await readFile(file.filePath), + }; + } + } catch (error) { + await this.setStatus( + row.id, + 'WAITING_ARTIFACT', + error instanceof Error ? error.message : 'Documento no disponible', + ); + return; + } + + await this.dataSource.query(` + UPDATE inspection_document_deliveries + SET attempts=attempts+1,last_attempt_at=CURRENT_TIMESTAMP,status='PENDING', + last_error=NULL,updated_at=CURRENT_TIMESTAMP + WHERE id=$1 + `, [row.id]); + + try { + const label = row.documentKind === 'ACT_PDF' + ? `Acta ${row.actCode}` + : `Informe ${row.reportCode ?? ''}`; + const text = row.documentKind === 'REPORT_WORD' + ? `Se adjunta el informe Word automático ${row.reportCode ?? ''} para revisión del Director de Hidrocarburos.` + : row.recipientKind === 'INSPECTOR' + ? `Se adjunta copia del acta cerrada e inmutable ${row.actCode} correspondiente a tu inspección.` + : `Se adjunta el acta cerrada e inmutable ${row.actCode}.`; + const sent = await this.smtp.send({ + to: row.recipientEmail, + subject: `DH Inspección · ${label}`, + text, + attachment, + }); + await this.dataSource.query(` + UPDATE inspection_document_deliveries + SET status='SENT',sent_at=CURRENT_TIMESTAMP,provider_message_id=$2, + last_error=NULL,updated_at=CURRENT_TIMESTAMP + WHERE id=$1 + `, [row.id, sent.messageId]); + await this.audit.record({ + action: AuditAction.DOCUMENT_DELIVERY_SENT, + entityType: 'inspection_document_delivery', + entityId: row.id, + source: AuditSource.SYSTEM, + actorUserId: null, + actorUsername: null, + requestId: null, + ip: null, + userAgent: null, + beforeData: null, + afterData: { + recipientKind: row.recipientKind, + recipientUserId: row.recipientUserId, + documentKind: row.documentKind, + status: 'SENT', + }, + metadata: { actId: row.actId, reportId: row.reportId }, + }); + } catch (error) { + await this.setStatus( + row.id, + 'FAILED', + error instanceof Error ? error.message : 'Error de entrega', + ); + } + } + + private async setStatus(id: string, status: string, error: string) { + await this.dataSource.query(` + UPDATE inspection_document_deliveries + SET status=$2,last_error=$3,updated_at=CURRENT_TIMESTAMP + WHERE id=$1 + `, [id, status, error.slice(0, 500)]); } - private async setStatus(id:string,status:string,error:string){await this.dataSource.query(`UPDATE inspection_document_deliveries SET status=$2,last_error=$3,updated_at=CURRENT_TIMESTAMP WHERE id=$1`,[id,status,error.slice(0,500)]);} }