F3.1: enviar copia del Acta al Inspector responsable
This commit is contained in:
@@ -1,40 +1,470 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
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 { 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';
|
||||||
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()
|
@Injectable()
|
||||||
export class InspectionDocumentDeliveryService {
|
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<string>('MAIL_FROM')??null}; }
|
async settings() {
|
||||||
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; }
|
const [row] = await this.dataSource.query(`
|
||||||
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}; }
|
SELECT
|
||||||
|
office_email AS "officeEmail",
|
||||||
async dispatchForAct(actId:string):Promise<void>{ 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); }
|
director_email AS "directorEmail",
|
||||||
async retry(id:string,principal:AuthPrincipal,request:RequestWithContext):Promise<DeliveryRow>{ 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); }
|
updated_at AS "updatedAt"
|
||||||
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}; }
|
FROM institutional_delivery_settings
|
||||||
|
WHERE id=1
|
||||||
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}>;
|
`) as Array<{
|
||||||
for(const company of companies)await this.upsertRow(actId,reportId,'ACT_PDF','COMPANY',company.id,company.email);
|
officeEmail: string | null;
|
||||||
await this.upsertRow(actId,reportId,'ACT_PDF','OFFICE',null,settings?.officeEmail??null);
|
directorEmail: string | null;
|
||||||
if(reportId)await this.upsertRow(actId,reportId,'REPORT_WORD','DIRECTOR',null,settings?.directorEmail??null);
|
updatedAt: Date;
|
||||||
|
}>;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
smtpConfigured: this.smtp.configured(),
|
||||||
|
mailFrom: this.config.get<string>('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<DeliveryRow[]>; }
|
async updateSettings(
|
||||||
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; }
|
dto: UpdateDocumentDeliverySettingsDto,
|
||||||
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]); }
|
principal: AuthPrincipal,
|
||||||
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;}
|
request: RequestWithContext,
|
||||||
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');}
|
) {
|
||||||
|
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<void> {
|
||||||
|
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<DeliveryRow> {
|
||||||
|
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<DeliveryRow[]> {
|
||||||
|
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<DeliveryRow[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async load(id: string): Promise<DeliveryRow> {
|
||||||
|
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)]);}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user