refactor(f4): deliver editable INF Word to inspector

This commit is contained in:
2026-09-07 20:58:49 -03:00
parent a1d994b6d4
commit 96093d9ed4
@@ -10,7 +10,7 @@ import { InspectionActPdfService } from './inspection-act-pdf.service';
import { InspectionReportWordService } from './inspection-report-word.service'; import { InspectionReportWordService } from './inspection-report-word.service';
import { SmtpDeliveryService } from './smtp-delivery.service'; import { SmtpDeliveryService } from './smtp-delivery.service';
export type DeliveryRecipientKind = 'COMPANY' | 'OFFICE' | 'DIRECTOR' | 'INSPECTOR'; export type DeliveryRecipientKind = 'COMPANY' | 'OFFICE' | 'INSPECTOR';
export interface DeliveryRow { export interface DeliveryRow {
id: string; id: string;
@@ -40,21 +40,14 @@ export class InspectionDocumentDeliveryService {
async settings() { async settings() {
const [row] = await this.dataSource.query(` const [row] = await this.dataSource.query(`
SELECT SELECT office_email AS "officeEmail",updated_at AS "updatedAt"
office_email AS "officeEmail",
director_email AS "directorEmail",
updated_at AS "updatedAt"
FROM institutional_delivery_settings FROM institutional_delivery_settings
WHERE id=1 WHERE id=1
`) as Array<{ `) as Array<{ officeEmail: string | null; updatedAt: Date }>;
officeEmail: string | null;
directorEmail: string | null;
updatedAt: Date;
}>;
return { return {
...row, ...row,
smtpConfigured: this.smtp.configured(), smtpConfigured: await this.smtp.configured(),
mailFrom: this.config.get<string>('MAIL_FROM') ?? null, mailFrom: await this.smtp.fromAddress() ?? this.config.get<string>('MAIL_FROM') ?? null,
}; };
} }
@@ -66,9 +59,9 @@ export class InspectionDocumentDeliveryService {
const before = await this.settings(); const before = await this.settings();
await this.dataSource.query(` await this.dataSource.query(`
UPDATE institutional_delivery_settings UPDATE institutional_delivery_settings
SET office_email=$1,director_email=$2,updated_by=$3,updated_at=CURRENT_TIMESTAMP SET office_email=$1,updated_by=$2,updated_at=CURRENT_TIMESTAMP
WHERE id=1 WHERE id=1
`, [dto.officeEmail ?? null, dto.directorEmail ?? null, principal.userId]); `, [dto.officeEmail ?? null, principal.userId]);
const after = await this.settings(); const after = await this.settings();
await this.audit.record({ await this.audit.record({
...administrationAuditContext(principal, request), ...administrationAuditContext(principal, request),
@@ -84,26 +77,15 @@ export class InspectionDocumentDeliveryService {
async list() { async list() {
const data = await this.dataSource.query(` const data = await this.dataSource.query(`
SELECT SELECT
d.id, d.id,d.act_id AS "actId",d.report_id AS "reportId",
d.act_id AS "actId", d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",
d.report_id AS "reportId", d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId",
d.document_kind AS "documentKind", d.recipient_email AS "recipientEmail",d.status,d.attempts,
d.recipient_kind AS "recipientKind", d.last_attempt_at AS "lastAttemptAt",d.sent_at AS "sentAt",
d.recipient_asset_id AS "recipientAssetId", d.provider_message_id AS "providerMessageId",d.last_error AS "lastError",
d.recipient_user_id AS "recipientUserId", d.created_at AS "createdAt",a.code AS "actCode",r.code AS "reportCode",
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", recipient.name AS "recipientAssetName",
CASE CASE WHEN recipient_user.id IS NULL THEN NULL
WHEN recipient_user.id IS NULL THEN NULL
ELSE btrim(concat_ws(' ',recipient_user.first_name,recipient_user.last_name)) ELSE btrim(concat_ws(' ',recipient_user.first_name,recipient_user.last_name))
END AS "recipientUserName" END AS "recipientUserName"
FROM inspection_document_deliveries d FROM inspection_document_deliveries d
@@ -111,6 +93,7 @@ export class InspectionDocumentDeliveryService {
LEFT JOIN inspection_reports r ON r.id=d.report_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 assets recipient ON recipient.id=d.recipient_asset_id
LEFT JOIN users recipient_user ON recipient_user.id=d.recipient_user_id LEFT JOIN users recipient_user ON recipient_user.id=d.recipient_user_id
WHERE d.recipient_kind<>'DIRECTOR'
ORDER BY d.created_at DESC ORDER BY d.created_at DESC
LIMIT 200 LIMIT 200
`); `);
@@ -154,11 +137,9 @@ export class InspectionDocumentDeliveryService {
async retryPending(principal: AuthPrincipal, request: RequestWithContext) { async retryPending(principal: AuthPrincipal, request: RequestWithContext) {
const rows = await this.dataSource.query(` const rows = await this.dataSource.query(`
SELECT id SELECT id FROM inspection_document_deliveries
FROM inspection_document_deliveries WHERE status<>'SENT' AND recipient_kind<>'DIRECTOR'
WHERE status<>'SENT' ORDER BY created_at ASC LIMIT 100
ORDER BY created_at ASC
LIMIT 100
`) as Array<{ id: string }>; `) as Array<{ id: string }>;
for (const item of rows) await this.retry(item.id, principal, request).catch(() => undefined); for (const item of rows) await this.retry(item.id, principal, request).catch(() => undefined);
return { processed: rows.length }; return { processed: rows.length };
@@ -166,10 +147,9 @@ export class InspectionDocumentDeliveryService {
private async ensureRows(actId: string, reportId: string | null) { private async ensureRows(actId: string, reportId: string | null) {
const [settings] = await this.dataSource.query(` const [settings] = await this.dataSource.query(`
SELECT office_email AS "officeEmail",director_email AS "directorEmail" SELECT office_email AS "officeEmail"
FROM institutional_delivery_settings FROM institutional_delivery_settings WHERE id=1
WHERE id=1 `) as Array<{ officeEmail: string | null }>;
`) as Array<{ officeEmail: string | null; directorEmail: string | null }>;
const companies = await this.dataSource.query(` const companies = await this.dataSource.query(`
SELECT DISTINCT company.id,profile.notification_email AS email SELECT DISTINCT company.id,profile.notification_email AS email
@@ -185,9 +165,7 @@ export class InspectionDocumentDeliveryService {
`, [actId]) as Array<{ id: string; email: string | null }>; `, [actId]) as Array<{ id: string; email: string | null }>;
const [inspector] = await this.dataSource.query(` const [inspector] = await this.dataSource.query(`
SELECT SELECT inspector.id,inspector.email
inspector.id,
inspector.email
FROM inspection_acts act FROM inspection_acts act
JOIN inspection_visits visit ON visit.id=act.visit_id JOIN inspection_visits visit ON visit.id=act.visit_id
JOIN users inspector ON inspector.id=visit.lead_inspector_user_id JOIN users inspector ON inspector.id=visit.lead_inspector_user_id
@@ -196,52 +174,32 @@ export class InspectionDocumentDeliveryService {
for (const company of companies) { for (const company of companies) {
await this.upsertRow({ await this.upsertRow({
actId, actId,reportId,documentKind:'ACT_PDF',recipientKind:'COMPANY',
reportId, recipientAssetId:company.id,recipientUserId:null,recipientKey:company.id,
documentKind: 'ACT_PDF',
recipientKind: 'COMPANY',
recipientAssetId: company.id,
recipientUserId: null,
recipientKey: company.id,
recipientEmail:company.email, recipientEmail:company.email,
}); });
} }
await this.upsertRow({ await this.upsertRow({
actId, actId,reportId,documentKind:'ACT_PDF',recipientKind:'OFFICE',
reportId, recipientAssetId:null,recipientUserId:null,
documentKind: 'ACT_PDF',
recipientKind: 'OFFICE',
recipientAssetId: null,
recipientUserId: null,
recipientKey:'00000000-0000-0000-0000-000000000000', recipientKey:'00000000-0000-0000-0000-000000000000',
recipientEmail:settings?.officeEmail ?? null, recipientEmail:settings?.officeEmail ?? null,
}); });
if (inspector) { if (inspector) {
await this.upsertRow({ await this.upsertRow({
actId, actId,reportId,documentKind:'ACT_PDF',recipientKind:'INSPECTOR',
reportId, recipientAssetId:null,recipientUserId:inspector.id,recipientKey:inspector.id,
documentKind: 'ACT_PDF', recipientEmail:inspector.email,
recipientKind: 'INSPECTOR', });
recipientAssetId: null, if (reportId) {
recipientUserId: inspector.id, await this.upsertRow({
recipientKey: inspector.id, actId,reportId,documentKind:'REPORT_WORD',recipientKind:'INSPECTOR',
recipientAssetId:null,recipientUserId:inspector.id,recipientKey:inspector.id,
recipientEmail:inspector.email, 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,
});
} }
} }
@@ -267,31 +225,21 @@ export class InspectionDocumentDeliveryService {
recipient_user_id=COALESCE(EXCLUDED.recipient_user_id,inspection_document_deliveries.recipient_user_id), recipient_user_id=COALESCE(EXCLUDED.recipient_user_id,inspection_document_deliveries.recipient_user_id),
recipient_email=CASE recipient_email=CASE
WHEN inspection_document_deliveries.status='SENT' THEN inspection_document_deliveries.recipient_email WHEN inspection_document_deliveries.status='SENT' THEN inspection_document_deliveries.recipient_email
ELSE EXCLUDED.recipient_email ELSE EXCLUDED.recipient_email END,
END,
status=CASE status=CASE
WHEN inspection_document_deliveries.status='SENT' THEN 'SENT' WHEN inspection_document_deliveries.status='SENT' THEN 'SENT'
WHEN EXCLUDED.recipient_email IS NULL THEN 'WAITING_RECIPIENT' WHEN EXCLUDED.recipient_email IS NULL THEN 'WAITING_RECIPIENT'
ELSE inspection_document_deliveries.status ELSE inspection_document_deliveries.status END,
END,
updated_at=CURRENT_TIMESTAMP updated_at=CURRENT_TIMESTAMP
`, [ `, [
input.actId, input.actId,input.reportId,input.documentKind,input.recipientKind,
input.reportId, input.recipientAssetId,input.recipientUserId,input.recipientKey,input.recipientEmail,initialStatus,
input.documentKind,
input.recipientKind,
input.recipientAssetId,
input.recipientUserId,
input.recipientKey,
input.recipientEmail,
initialStatus,
]); ]);
} }
private async rowsForAct(actId: string): Promise<DeliveryRow[]> { private async rowsForAct(actId: string): Promise<DeliveryRow[]> {
return this.dataSource.query(` return this.dataSource.query(`
SELECT SELECT d.id,d.act_id AS "actId",d.report_id AS "reportId",
d.id,d.act_id AS "actId",d.report_id AS "reportId",
d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind", d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",
d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId", d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId",
d.recipient_email AS "recipientEmail",d.status,d.attempts, d.recipient_email AS "recipientEmail",d.status,d.attempts,
@@ -299,15 +247,14 @@ export class InspectionDocumentDeliveryService {
FROM inspection_document_deliveries d FROM inspection_document_deliveries d
JOIN inspection_acts a ON a.id=d.act_id JOIN inspection_acts a ON a.id=d.act_id
LEFT JOIN inspection_reports r ON r.id=d.report_id LEFT JOIN inspection_reports r ON r.id=d.report_id
WHERE d.act_id=$1 WHERE d.act_id=$1 AND d.recipient_kind<>'DIRECTOR'
ORDER BY d.created_at ORDER BY d.created_at
`, [actId]) as Promise<DeliveryRow[]>; `, [actId]) as Promise<DeliveryRow[]>;
} }
private async load(id: string): Promise<DeliveryRow> { private async load(id: string): Promise<DeliveryRow> {
const [row] = await this.dataSource.query(` const [row] = await this.dataSource.query(`
SELECT SELECT d.id,d.act_id AS "actId",d.report_id AS "reportId",
d.id,d.act_id AS "actId",d.report_id AS "reportId",
d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind", d.document_kind AS "documentKind",d.recipient_kind AS "recipientKind",
d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId", d.recipient_asset_id AS "recipientAssetId",d.recipient_user_id AS "recipientUserId",
d.recipient_email AS "recipientEmail",d.status,d.attempts, d.recipient_email AS "recipientEmail",d.status,d.attempts,
@@ -315,12 +262,11 @@ export class InspectionDocumentDeliveryService {
FROM inspection_document_deliveries d FROM inspection_document_deliveries d
JOIN inspection_acts a ON a.id=d.act_id JOIN inspection_acts a ON a.id=d.act_id
LEFT JOIN inspection_reports r ON r.id=d.report_id LEFT JOIN inspection_reports r ON r.id=d.report_id
WHERE d.id=$1 WHERE d.id=$1 AND d.recipient_kind<>'DIRECTOR'
`, [id]) as DeliveryRow[]; `, [id]) as DeliveryRow[];
if (!row) { if (!row) {
throw new NotFoundException({ throw new NotFoundException({
code: 'DOCUMENT_DELIVERY_NOT_FOUND', code:'DOCUMENT_DELIVERY_NOT_FOUND',message:'Entrega documental no encontrada',
message: 'Entrega documental no encontrada',
}); });
} }
return row; return row;
@@ -330,35 +276,25 @@ export class InspectionDocumentDeliveryService {
let email: string | null = null; let email: string | null = null;
if (row.recipientKind === 'COMPANY' && row.recipientAssetId) { if (row.recipientKind === 'COMPANY' && row.recipientAssetId) {
const [company] = await this.dataSource.query(` const [company] = await this.dataSource.query(`
SELECT notification_email AS email SELECT notification_email AS email FROM organization_profiles WHERE asset_id=$1
FROM organization_profiles
WHERE asset_id=$1
`, [row.recipientAssetId]) as Array<{ email: string | null }>; `, [row.recipientAssetId]) as Array<{ email: string | null }>;
email = company?.email ?? null; email = company?.email ?? null;
} else if (row.recipientKind === 'INSPECTOR' && row.recipientUserId) { } else if (row.recipientKind === 'INSPECTOR' && row.recipientUserId) {
const [inspector] = await this.dataSource.query(` const [inspector] = await this.dataSource.query(`
SELECT email SELECT email FROM users WHERE id=$1 AND is_active=true
FROM users
WHERE id=$1 AND is_active=true
`, [row.recipientUserId]) as Array<{ email: string | null }>; `, [row.recipientUserId]) as Array<{ email: string | null }>;
email = inspector?.email ?? null; email = inspector?.email ?? null;
} else { } else {
const [settings] = await this.dataSource.query(` const [settings] = await this.dataSource.query(`
SELECT office_email AS "officeEmail",director_email AS "directorEmail" SELECT office_email AS "officeEmail" FROM institutional_delivery_settings WHERE id=1
FROM institutional_delivery_settings `) as Array<{ officeEmail: string | null }>;
WHERE id=1 email = settings?.officeEmail ?? null;
`) as Array<{ officeEmail: string | null; directorEmail: string | null }>;
email = row.recipientKind === 'OFFICE'
? settings?.officeEmail ?? null
: settings?.directorEmail ?? null;
} }
await this.dataSource.query(` await this.dataSource.query(`
UPDATE inspection_document_deliveries UPDATE inspection_document_deliveries
SET recipient_email=$2, SET recipient_email=$2,
status=CASE WHEN $2::text IS NULL THEN 'WAITING_RECIPIENT' ELSE 'PENDING' END, status=CASE WHEN $2::text IS NULL THEN 'WAITING_RECIPIENT' ELSE 'PENDING' END,
last_error=NULL, last_error=NULL,updated_at=CURRENT_TIMESTAMP
updated_at=CURRENT_TIMESTAMP
WHERE id=$1 AND status<>'SENT' WHERE id=$1 AND status<>'SENT'
`, [row.id,email]); `, [row.id,email]);
} }
@@ -369,7 +305,7 @@ export class InspectionDocumentDeliveryService {
await this.setStatus(row.id,'WAITING_RECIPIENT','Destinatario no configurado'); await this.setStatus(row.id,'WAITING_RECIPIENT','Destinatario no configurado');
return; return;
} }
if (!this.smtp.configured()) { if (!await this.smtp.configured()) {
await this.setStatus(row.id,'WAITING_TRANSPORT','SMTP no configurado'); await this.setStatus(row.id,'WAITING_TRANSPORT','SMTP no configurado');
return; return;
} }
@@ -379,28 +315,18 @@ export class InspectionDocumentDeliveryService {
if (row.documentKind === 'ACT_PDF') { if (row.documentKind === 'ACT_PDF') {
await this.pdf.ensure(row.actId); await this.pdf.ensure(row.actId);
const file = await this.pdf.content(row.actId); const file = await this.pdf.content(row.actId);
attachment = { attachment = { filename:file.originalName,mimeType:file.mimeType,content:file.buffer };
filename: file.originalName,
mimeType: file.mimeType,
content: file.buffer,
};
} else { } else {
if (!row.reportId) throw new Error('Informe no vinculado'); if (!row.reportId) throw new Error('Informe no vinculado');
await this.word.ensure(row.reportId); await this.word.ensure(row.reportId);
const file = await this.word.content(row.reportId); const file = await this.word.content(row.reportId);
const { readFile } = await import('node:fs/promises'); const { readFile } = await import('node:fs/promises');
attachment = { attachment = {
filename: file.originalName, filename:file.originalName,mimeType:file.mimeType,content:await readFile(file.filePath),
mimeType: file.mimeType,
content: await readFile(file.filePath),
}; };
} }
} catch (error) { } catch (error) {
await this.setStatus( await this.setStatus(row.id,'WAITING_ARTIFACT',error instanceof Error ? error.message : 'Documento no disponible');
row.id,
'WAITING_ARTIFACT',
error instanceof Error ? error.message : 'Documento no disponible',
);
return; return;
} }
@@ -416,55 +342,34 @@ export class InspectionDocumentDeliveryService {
? `Acta ${row.actCode}` ? `Acta ${row.actCode}`
: `Informe ${row.reportCode ?? ''}`; : `Informe ${row.reportCode ?? ''}`;
const text = row.documentKind === 'REPORT_WORD' const text = row.documentKind === 'REPORT_WORD'
? `Se adjunta el informe Word automático ${row.reportCode ?? ''} para revisión del Director de Hidrocarburos.` ? `Se adjunta el INF editable ${row.reportCode ?? ''} para su revisión y preparación antes de cargarlo en GEDO.`
: row.recipientKind === 'INSPECTOR' : row.recipientKind === 'INSPECTOR'
? `Se adjunta copia del acta cerrada e inmutable ${row.actCode} correspondiente a tu inspección.` ? `Se adjunta copia del acta sellada e inmutable ${row.actCode} correspondiente a tu inspección.`
: `Se adjunta el acta cerrada e inmutable ${row.actCode}.`; : `Se adjunta el acta sellada e inmutable ${row.actCode}.`;
const sent = await this.smtp.send({ const sent = await this.smtp.send({
to: row.recipientEmail, to:row.recipientEmail,subject:`DH Inspección · ${label}`,text,attachment,
subject: `DH Inspección · ${label}`,
text,
attachment,
}); });
await this.dataSource.query(` await this.dataSource.query(`
UPDATE inspection_document_deliveries UPDATE inspection_document_deliveries
SET status='SENT',sent_at=CURRENT_TIMESTAMP,provider_message_id=$2, SET status='SENT',sent_at=CURRENT_TIMESTAMP,provider_message_id=$2,
last_error=NULL,updated_at=CURRENT_TIMESTAMP last_error=NULL,updated_at=CURRENT_TIMESTAMP WHERE id=$1
WHERE id=$1
`,[row.id,sent.messageId]); `,[row.id,sent.messageId]);
await this.audit.record({ await this.audit.record({
action:AuditAction.DOCUMENT_DELIVERY_SENT, action:AuditAction.DOCUMENT_DELIVERY_SENT,
entityType: 'inspection_document_delivery', entityType:'inspection_document_delivery',entityId:row.id,source:AuditSource.SYSTEM,
entityId: row.id, actorUserId:null,actorUsername:null,requestId:null,ip:null,userAgent:null,beforeData:null,
source: AuditSource.SYSTEM, afterData:{recipientKind:row.recipientKind,recipientUserId:row.recipientUserId,documentKind:row.documentKind,status:'SENT'},
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}, metadata:{actId:row.actId,reportId:row.reportId},
}); });
} catch (error) { } catch (error) {
await this.setStatus( await this.setStatus(row.id,'FAILED',error instanceof Error ? error.message : 'Error de entrega');
row.id,
'FAILED',
error instanceof Error ? error.message : 'Error de entrega',
);
} }
} }
private async setStatus(id: string,status: string,error: string) { private async setStatus(id: string,status: string,error: string) {
await this.dataSource.query(` await this.dataSource.query(`
UPDATE inspection_document_deliveries UPDATE inspection_document_deliveries
SET status=$2,last_error=$3,updated_at=CURRENT_TIMESTAMP SET status=$2,last_error=$3,updated_at=CURRENT_TIMESTAMP WHERE id=$1
WHERE id=$1
`,[id,status,error.slice(0,500)]); `,[id,status,error.slice(0,500)]);
} }
} }